letsencrypt-0.4.1/0000755000175000017500000000000012665157717013505 5ustar bmwbmw00000000000000letsencrypt-0.4.1/examples/0000755000175000017500000000000012665157717015323 5ustar bmwbmw00000000000000letsencrypt-0.4.1/examples/.gitignore0000644000175000017500000000004412665157707017310 0ustar bmwbmw00000000000000# generate-csr.sh: /key.pem /csr.derletsencrypt-0.4.1/examples/dev-cli.ini0000644000175000017500000000102612665157707017345 0ustar bmwbmw00000000000000# Always use the staging/testing server - avoids rate limiting server = https://acme-staging.api.letsencrypt.org/directory # This is an example configuration file for developers config-dir = /tmp/le/conf work-dir = /tmp/le/conf logs-dir = /tmp/le/logs # make sure to use a valid email and domains! email = foo@example.com domains = example.com text = True agree-tos = True debug = True # Unfortunately, it's not possible to specify "verbose" multiple times # (correspondingly to -vvvvvv) verbose = True authenticator = standalone letsencrypt-0.4.1/examples/cli.ini0000644000175000017500000000162112665157707016572 0ustar bmwbmw00000000000000# This is an example of the kind of things you can do in a configuration file. # All flags used by the client can be configured here. Run Let's Encrypt with # "--help" to learn more about the available options. # Use a 4096 bit RSA key instead of 2048 rsa-key-size = 4096 # Uncomment and update to register with the specified e-mail address # email = foo@example.com # Uncomment and update to generate certificates for the specified # domains. # domains = example.com, www.example.com # Uncomment to use a text interface instead of ncurses # text = True # Uncomment to use the standalone authenticator on port 443 # authenticator = standalone # standalone-supported-challenges = tls-sni-01 # Uncomment to use the webroot authenticator. Replace webroot-path with the # path to the public_html / webroot folder being served by your web server. # authenticator = webroot # webroot-path = /usr/share/nginx/html letsencrypt-0.4.1/examples/generate-csr.sh0000755000175000017500000000132512665157707020241 0ustar bmwbmw00000000000000#!/bin/sh # This script generates a simple SAN CSR to be used with Let's Encrypt # CA. Mostly intended for "auth --csr" testing, but, since it's easily # auditable, feel free to adjust it and use it on your production web # server. if [ "$#" -lt 1 ] then echo "Usage: $0 domain [domain...]" >&2 exit 1 fi domains="DNS:$1" shift for x in "$@" do domains="$domains,DNS:$x" done SAN="$domains" openssl req -config "${OPENSSL_CNF:-openssl.cnf}" \ -new -nodes -subj '/' -reqexts san \ -out "${CSR_PATH:-csr.der}" \ -keyout "${KEY_PATH:-key.pem}" \ -newkey rsa:2048 \ -outform DER # 512 or 1024 too low for Boulder, 2048 is smallest for tests echo "You can now run: letsencrypt auth --csr ${CSR_PATH:-csr.der}" letsencrypt-0.4.1/examples/openssl.cnf0000644000175000017500000000016212665157707017474 0ustar bmwbmw00000000000000[ req ] distinguished_name = req_distinguished_name [ req_distinguished_name ] [ san ] subjectAltName=${ENV::SAN} letsencrypt-0.4.1/examples/plugins/0000755000175000017500000000000012665157717017004 5ustar bmwbmw00000000000000letsencrypt-0.4.1/examples/plugins/letsencrypt_example_plugins.py0000644000175000017500000000155712665157707025215 0ustar bmwbmw00000000000000"""Example Let's Encrypt plugins. For full examples, see `letsencrypt.plugins`. """ import zope.interface from letsencrypt import interfaces from letsencrypt.plugins import common @zope.interface.implementer(interfaces.IAuthenticator) @zope.interface.provider(interfaces.IPluginFactory) class Authenticator(common.Plugin): """Example Authenticator.""" description = "Example Authenticator plugin" # Implement all methods from IAuthenticator, remembering to add # "self" as first argument, e.g. def prepare(self)... @zope.interface.implementer(interfaces.IInstaller) @zope.interface.provider(interfaces.IPluginFactory) class Installer(common.Plugin): """Example Installer.""" description = "Example Installer plugin" # Implement all methods from IInstaller, remembering to add # "self" as first argument, e.g. def get_all_names(self)... letsencrypt-0.4.1/examples/plugins/setup.py0000644000175000017500000000065512665157707020523 0ustar bmwbmw00000000000000from setuptools import setup setup( name='letsencrypt-example-plugins', package='letsencrypt_example_plugins.py', install_requires=[ 'letsencrypt', 'zope.interface', ], entry_points={ 'letsencrypt.plugins': [ 'example_authenticator = letsencrypt_example_plugins:Authenticator', 'example_installer = letsencrypt_example_plugins:Installer', ], }, ) letsencrypt-0.4.1/letsencrypt.egg-info/0000755000175000017500000000000012665157717017553 5ustar bmwbmw00000000000000letsencrypt-0.4.1/letsencrypt.egg-info/dependency_links.txt0000644000175000017500000000000112665157717023621 0ustar bmwbmw00000000000000 letsencrypt-0.4.1/letsencrypt.egg-info/top_level.txt0000644000175000017500000000001412665157717022300 0ustar bmwbmw00000000000000letsencrypt letsencrypt-0.4.1/letsencrypt.egg-info/requires.txt0000644000175000017500000000056712665157717022163 0ustar bmwbmw00000000000000acme==0.4.1 ConfigArgParse>=0.9.3 configobj cryptography>=0.7 parsedatetime<2.0 psutil>=2.1.0 PyOpenSSL pyrfc3339 python2-pythondialog>=3.2.2rc1 pytz setuptools six zope.component zope.interface mock [dev] astroid==1.3.5 coverage nose nosexcover pep8 pylint==1.4.2 tox twine wheel [docs] repoze.sphinx.autointerface Sphinx>=1.0 sphinx_rtd_theme sphinxcontrib-programoutput letsencrypt-0.4.1/letsencrypt.egg-info/entry_points.txt0000644000175000017500000000042712665157717023054 0ustar bmwbmw00000000000000[console_scripts] letsencrypt = letsencrypt.cli:main [letsencrypt.plugins] manual = letsencrypt.plugins.manual:Authenticator null = letsencrypt.plugins.null:Installer standalone = letsencrypt.plugins.standalone:Authenticator webroot = letsencrypt.plugins.webroot:Authenticator letsencrypt-0.4.1/letsencrypt.egg-info/PKG-INFO0000644000175000017500000002140612665157717020653 0ustar bmwbmw00000000000000Metadata-Version: 1.1 Name: letsencrypt Version: 0.4.1 Summary: Let's Encrypt client Home-page: https://github.com/letsencrypt/letsencrypt Author: Let's Encrypt Project Author-email: client-dev@letsencrypt.org License: Apache License 2.0 Description: .. notice for github users Disclaimer ========== The Let's Encrypt Client is **BETA SOFTWARE**. It contains plenty of bugs and rough edges, and should be tested thoroughly in staging environments before use on production systems. For more information regarding the status of the project, please see https://letsencrypt.org. Be sure to checkout the `Frequently Asked Questions (FAQ) `_. About the Let's Encrypt Client ============================== The Let's Encrypt Client is a fully-featured, extensible client for the Let's Encrypt CA (or any other CA that speaks the `ACME `_ protocol) that can automate the tasks of obtaining certificates and configuring webservers to use them. Installation ------------ If ``letsencrypt`` is packaged for your OS, you can install it from there, and run it by typing ``letsencrypt``. Because not all operating systems have packages yet, we provide a temporary solution via the ``letsencrypt-auto`` wrapper script, which obtains some dependencies from your OS and puts others in a python virtual environment:: user@webserver:~$ git clone https://github.com/letsencrypt/letsencrypt user@webserver:~$ cd letsencrypt user@webserver:~/letsencrypt$ ./letsencrypt-auto --help Or for full command line help, type:: ./letsencrypt-auto --help all ``letsencrypt-auto`` updates to the latest client release automatically. And since ``letsencrypt-auto`` is a wrapper to ``letsencrypt``, it accepts exactly the same command line flags and arguments. More details about this script and other installation methods can be found `in the User Guide `_. How to run the client --------------------- In many cases, you can just run ``letsencrypt-auto`` or ``letsencrypt``, and the client will guide you through the process of obtaining and installing certs interactively. You can also tell it exactly what you want it to do from the command line. For instance, if you want to obtain a cert for ``example.com``, ``www.example.com``, and ``other.example.net``, using the Apache plugin to both obtain and install the certs, you could do this:: ./letsencrypt-auto --apache -d example.com -d www.example.com -d other.example.net (The first time you run the command, it will make an account, and ask for an email and agreement to the Let's Encrypt Subscriber Agreement; you can automate those with ``--email`` and ``--agree-tos``) If you want to use a webserver that doesn't have full plugin support yet, you can still use "standalone" or "webroot" plugins to obtain a certificate:: ./letsencrypt-auto certonly --standalone --email admin@example.com -d example.com -d www.example.com -d other.example.net Understanding the client in more depth -------------------------------------- To understand what the client is doing in detail, it's important to understand the way it uses plugins. Please see the `explanation of plugins `_ in the User Guide. Links ===== Documentation: https://letsencrypt.readthedocs.org Software project: https://github.com/letsencrypt/letsencrypt Notes for developers: https://letsencrypt.readthedocs.org/en/latest/contributing.html Main Website: https://letsencrypt.org/ IRC Channel: #letsencrypt on `Freenode`_ Community: https://community.letsencrypt.org Mailing list: `client-dev`_ (to subscribe without a Google account, send an email to client-dev+subscribe@letsencrypt.org) |build-status| |coverage| |docs| |container| .. |build-status| image:: https://travis-ci.org/letsencrypt/letsencrypt.svg?branch=master :target: https://travis-ci.org/letsencrypt/letsencrypt :alt: Travis CI status .. |coverage| image:: https://coveralls.io/repos/letsencrypt/letsencrypt/badge.svg?branch=master :target: https://coveralls.io/r/letsencrypt/letsencrypt :alt: Coverage status .. |docs| image:: https://readthedocs.org/projects/letsencrypt/badge/ :target: https://readthedocs.org/projects/letsencrypt/ :alt: Documentation status .. |container| image:: https://quay.io/repository/letsencrypt/letsencrypt/status :target: https://quay.io/repository/letsencrypt/letsencrypt :alt: Docker Repository on Quay.io .. _`installation instructions`: https://letsencrypt.readthedocs.org/en/latest/using.html .. _watch demo video: https://www.youtube.com/watch?v=Gas_sSB-5SU System Requirements =================== The Let's Encrypt Client presently only runs on Unix-ish OSes that include Python 2.6 or 2.7; Python 3.x support will be added after the Public Beta launch. The client requires root access in order to write to ``/etc/letsencrypt``, ``/var/log/letsencrypt``, ``/var/lib/letsencrypt``; to bind to ports 80 and 443 (if you use the ``standalone`` plugin) and to read and modify webserver configurations (if you use the ``apache`` or ``nginx`` plugins). If none of these apply to you, it is theoretically possible to run without root privileges, but for most users who want to avoid running an ACME client as root, either `letsencrypt-nosudo `_ or `simp_le `_ are more appropriate choices. The Apache plugin currently requires a Debian-based OS with augeas version 1.0; this includes Ubuntu 12.04+ and Debian 7+. Current Features ================ * Supports multiple web servers: - apache/2.x (working on Debian 8+ and Ubuntu 12.04+) - standalone (runs its own simple webserver to prove you control a domain) - webroot (adds files to webroot directories in order to prove control of domains and obtain certs) - nginx/0.8.48+ (highly experimental, not included in letsencrypt-auto) * The private key is generated locally on your system. * Can talk to the Let's Encrypt CA or optionally to other ACME compliant services. * Can get domain-validated (DV) certificates. * Can revoke certificates. * Adjustable RSA key bit-length (2048 (default), 4096, ...). * Can optionally install a http -> https redirect, so your site effectively runs https only (Apache only) * Fully automated. * Configuration changes are logged and can be reverted. * Supports ncurses and text (-t) UI, or can be driven entirely from the command line. * Free and Open Source Software, made with Python. .. _Freenode: https://webchat.freenode.net?channels=%23letsencrypt .. _client-dev: https://groups.google.com/a/letsencrypt.org/forum/#!forum/client-dev Platform: UNKNOWN Classifier: Development Status :: 3 - Alpha Classifier: Environment :: Console Classifier: Environment :: Console :: Curses Classifier: Intended Audience :: System Administrators Classifier: License :: OSI Approved :: Apache Software License Classifier: Operating System :: POSIX :: Linux Classifier: Programming Language :: Python Classifier: Programming Language :: Python :: 2 Classifier: Programming Language :: Python :: 2.6 Classifier: Programming Language :: Python :: 2.7 Classifier: Topic :: Internet :: WWW/HTTP Classifier: Topic :: Security Classifier: Topic :: System :: Installation/Setup Classifier: Topic :: System :: Networking Classifier: Topic :: System :: Systems Administration Classifier: Topic :: Utilities letsencrypt-0.4.1/letsencrypt.egg-info/SOURCES.txt0000644000175000017500000001100712665157717021436 0ustar bmwbmw00000000000000CHANGES.rst CONTRIBUTING.md LICENSE.txt MANIFEST.in README.rst linter_plugin.py setup.cfg setup.py docs/.gitignore docs/Makefile docs/api.rst docs/ciphers.rst docs/conf.py docs/contributing.rst docs/index.rst docs/intro.rst docs/make.bat docs/packaging.rst docs/using.rst docs/_static/.gitignore docs/api/account.rst docs/api/achallenges.rst docs/api/auth_handler.rst docs/api/client.rst docs/api/configuration.rst docs/api/constants.rst docs/api/continuity_auth.rst docs/api/crypto_util.rst docs/api/display.rst docs/api/errors.rst docs/api/index.rst docs/api/interfaces.rst docs/api/le_util.rst docs/api/log.rst docs/api/proof_of_possession.rst docs/api/reporter.rst docs/api/reverter.rst docs/api/storage.rst docs/api/plugins/common.rst docs/api/plugins/disco.rst docs/api/plugins/manual.rst docs/api/plugins/standalone.rst docs/api/plugins/util.rst docs/api/plugins/webroot.rst docs/man/letsencrypt.rst examples/.gitignore examples/cli.ini examples/dev-cli.ini examples/generate-csr.sh examples/openssl.cnf examples/plugins/letsencrypt_example_plugins.py examples/plugins/setup.py letsencrypt/__init__.py letsencrypt/account.py letsencrypt/achallenges.py letsencrypt/auth_handler.py letsencrypt/cli.py letsencrypt/client.py letsencrypt/colored_logging.py letsencrypt/configuration.py letsencrypt/constants.py letsencrypt/continuity_auth.py letsencrypt/crypto_util.py letsencrypt/error_handler.py letsencrypt/errors.py letsencrypt/interfaces.py letsencrypt/le_util.py letsencrypt/log.py letsencrypt/notify.py letsencrypt/proof_of_possession.py letsencrypt/reporter.py letsencrypt/reverter.py letsencrypt/storage.py letsencrypt.egg-info/PKG-INFO letsencrypt.egg-info/SOURCES.txt letsencrypt.egg-info/dependency_links.txt letsencrypt.egg-info/entry_points.txt letsencrypt.egg-info/requires.txt letsencrypt.egg-info/top_level.txt letsencrypt/display/__init__.py letsencrypt/display/enhancements.py letsencrypt/display/ops.py letsencrypt/display/util.py letsencrypt/plugins/__init__.py letsencrypt/plugins/common.py letsencrypt/plugins/common_test.py letsencrypt/plugins/disco.py letsencrypt/plugins/disco_test.py letsencrypt/plugins/manual.py letsencrypt/plugins/manual_test.py letsencrypt/plugins/null.py letsencrypt/plugins/null_test.py letsencrypt/plugins/standalone.py letsencrypt/plugins/standalone_test.py letsencrypt/plugins/util.py letsencrypt/plugins/util_test.py letsencrypt/plugins/webroot.py letsencrypt/plugins/webroot_test.py letsencrypt/tests/__init__.py letsencrypt/tests/account_test.py letsencrypt/tests/acme_util.py letsencrypt/tests/auth_handler_test.py letsencrypt/tests/cli_test.py letsencrypt/tests/client_test.py letsencrypt/tests/colored_logging_test.py letsencrypt/tests/configuration_test.py letsencrypt/tests/continuity_auth_test.py letsencrypt/tests/crypto_util_test.py letsencrypt/tests/error_handler_test.py letsencrypt/tests/errors_test.py letsencrypt/tests/le_util_test.py letsencrypt/tests/log_test.py letsencrypt/tests/notify_test.py letsencrypt/tests/proof_of_possession_test.py letsencrypt/tests/reporter_test.py letsencrypt/tests/reverter_test.py letsencrypt/tests/storage_test.py letsencrypt/tests/test_util.py letsencrypt/tests/display/__init__.py letsencrypt/tests/display/enhancements_test.py letsencrypt/tests/display/ops_test.py letsencrypt/tests/display/util_test.py letsencrypt/tests/testdata/cert-san.pem letsencrypt/tests/testdata/cert.b64jose letsencrypt/tests/testdata/cert.der letsencrypt/tests/testdata/cert.pem letsencrypt/tests/testdata/cli.ini letsencrypt/tests/testdata/csr-6sans.pem letsencrypt/tests/testdata/csr-nosans.pem letsencrypt/tests/testdata/csr-san.der letsencrypt/tests/testdata/csr-san.pem letsencrypt/tests/testdata/csr.der letsencrypt/tests/testdata/csr.pem letsencrypt/tests/testdata/dsa512_key.pem letsencrypt/tests/testdata/dsa_cert.pem letsencrypt/tests/testdata/matching_cert.pem letsencrypt/tests/testdata/rsa256_key.pem letsencrypt/tests/testdata/rsa512_key.pem letsencrypt/tests/testdata/rsa512_key_2.pem letsencrypt/tests/testdata/sample-renewal-ancient.conf letsencrypt/tests/testdata/sample-renewal.conf letsencrypt/tests/testdata/webrootconftest.ini letsencrypt/tests/testdata/archive/sample-renewal/cert1.pem letsencrypt/tests/testdata/archive/sample-renewal/chain1.pem letsencrypt/tests/testdata/archive/sample-renewal/fullchain1.pem letsencrypt/tests/testdata/archive/sample-renewal/privkey1.pem letsencrypt/tests/testdata/live/sample-renewal/cert.pem letsencrypt/tests/testdata/live/sample-renewal/chain.pem letsencrypt/tests/testdata/live/sample-renewal/fullchain.pem letsencrypt/tests/testdata/live/sample-renewal/privkey.pemletsencrypt-0.4.1/setup.py0000644000175000017500000001044212665157707015217 0ustar bmwbmw00000000000000import codecs import os import re import sys from setuptools import setup from setuptools import find_packages # Workaround for http://bugs.python.org/issue8876, see # http://bugs.python.org/issue8876#msg208792 # This can be removed when using Python 2.7.9 or later: # https://hg.python.org/cpython/raw-file/v2.7.9/Misc/NEWS if os.path.abspath(__file__).split(os.path.sep)[1] == 'vagrant': del os.link def read_file(filename, encoding='utf8'): """Read unicode from given file.""" with codecs.open(filename, encoding=encoding) as fd: return fd.read() here = os.path.abspath(os.path.dirname(__file__)) # read version number (and other metadata) from package init init_fn = os.path.join(here, 'letsencrypt', '__init__.py') meta = dict(re.findall(r"""__([a-z]+)__ = '([^']+)""", read_file(init_fn))) readme = read_file(os.path.join(here, 'README.rst')) changes = read_file(os.path.join(here, 'CHANGES.rst')) version = meta['version'] # Please update tox.ini when modifying dependency version requirements install_requires = [ 'acme=={0}'.format(version), # We technically need ConfigArgParse 0.10.0 for Python 2.6 support, but # saying so here causes a runtime error against our temporary fork of 0.9.3 # in which we added 2.6 support (see #2243), so we relax the requirement. 'ConfigArgParse>=0.9.3', 'configobj', 'cryptography>=0.7', # load_pem_x509_certificate 'parsedatetime<2.0', # parsedatetime 2.0 doesn't work on py26 'psutil>=2.1.0', # net_connections introduced in 2.1.0 'PyOpenSSL', 'pyrfc3339', 'python2-pythondialog>=3.2.2rc1', # Debian squeeze support, cf. #280 'pytz', 'setuptools', # pkg_resources 'six', 'zope.component', 'zope.interface', ] # env markers in extras_require cause problems with older pip: #517 # Keep in sync with conditional_requirements.py. if sys.version_info < (2, 7): install_requires.extend([ # only some distros recognize stdlib argparse as already satisfying 'argparse', 'mock<1.1.0', ]) else: install_requires.append('mock') dev_extras = [ # Pin astroid==1.3.5, pylint==1.4.2 as a workaround for #289 'astroid==1.3.5', 'coverage', 'nose', 'nosexcover', 'pep8', 'pylint==1.4.2', # upstream #248 'tox', 'twine', 'wheel', ] docs_extras = [ 'repoze.sphinx.autointerface', 'Sphinx>=1.0', # autodoc_member_order = 'bysource', autodoc_default_flags 'sphinx_rtd_theme', 'sphinxcontrib-programoutput', ] setup( name='letsencrypt', version=version, description="Let's Encrypt client", long_description=readme, # later: + '\n\n' + changes url='https://github.com/letsencrypt/letsencrypt', author="Let's Encrypt Project", author_email='client-dev@letsencrypt.org', license='Apache License 2.0', classifiers=[ 'Development Status :: 3 - Alpha', 'Environment :: Console', 'Environment :: Console :: Curses', 'Intended Audience :: System Administrators', 'License :: OSI Approved :: Apache Software License', 'Operating System :: POSIX :: Linux', 'Programming Language :: Python', 'Programming Language :: Python :: 2', 'Programming Language :: Python :: 2.6', 'Programming Language :: Python :: 2.7', 'Topic :: Internet :: WWW/HTTP', 'Topic :: Security', 'Topic :: System :: Installation/Setup', 'Topic :: System :: Networking', 'Topic :: System :: Systems Administration', 'Topic :: Utilities', ], packages=find_packages(exclude=['docs', 'examples', 'tests', 'venv']), include_package_data=True, install_requires=install_requires, extras_require={ 'dev': dev_extras, 'docs': docs_extras, }, # to test all packages run "python setup.py test -s # {acme,letsencrypt_apache,letsencrypt_nginx}" test_suite='letsencrypt', entry_points={ 'console_scripts': [ 'letsencrypt = letsencrypt.cli:main', ], 'letsencrypt.plugins': [ 'manual = letsencrypt.plugins.manual:Authenticator', 'null = letsencrypt.plugins.null:Installer', 'standalone = letsencrypt.plugins.standalone:Authenticator', 'webroot = letsencrypt.plugins.webroot:Authenticator', ], }, ) letsencrypt-0.4.1/docs/0000755000175000017500000000000012665157717014435 5ustar bmwbmw00000000000000letsencrypt-0.4.1/docs/.gitignore0000644000175000017500000000001112665157707016414 0ustar bmwbmw00000000000000/_build/ letsencrypt-0.4.1/docs/man/0000755000175000017500000000000012665157717015210 5ustar bmwbmw00000000000000letsencrypt-0.4.1/docs/man/letsencrypt.rst0000644000175000017500000000005312665157707020313 0ustar bmwbmw00000000000000.. program-output:: letsencrypt --help all letsencrypt-0.4.1/docs/api.rst0000644000175000017500000000013112665157707015732 0ustar bmwbmw00000000000000================= API Documentation ================= .. toctree:: :glob: api/** letsencrypt-0.4.1/docs/make.bat0000644000175000017500000001612612665157707016047 0ustar bmwbmw00000000000000@ECHO OFF REM Command file for Sphinx documentation if "%SPHINXBUILD%" == "" ( set SPHINXBUILD=sphinx-build ) set BUILDDIR=_build set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% . set I18NSPHINXOPTS=%SPHINXOPTS% . if NOT "%PAPER%" == "" ( set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS% set I18NSPHINXOPTS=-D latex_paper_size=%PAPER% %I18NSPHINXOPTS% ) if "%1" == "" goto help if "%1" == "help" ( :help echo.Please use `make ^` where ^ is one of echo. html to make standalone HTML files echo. dirhtml to make HTML files named index.html in directories echo. singlehtml to make a single large HTML file echo. pickle to make pickle files echo. json to make JSON files echo. htmlhelp to make HTML files and a HTML help project echo. qthelp to make HTML files and a qthelp project echo. devhelp to make HTML files and a Devhelp project echo. epub to make an epub echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter echo. text to make text files echo. man to make manual pages echo. texinfo to make Texinfo files echo. gettext to make PO message catalogs echo. changes to make an overview over all changed/added/deprecated items echo. xml to make Docutils-native XML files echo. pseudoxml to make pseudoxml-XML files for display purposes echo. linkcheck to check all external links for integrity echo. doctest to run all doctests embedded in the documentation if enabled echo. coverage to run coverage check of the documentation if enabled goto end ) if "%1" == "clean" ( for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i del /q /s %BUILDDIR%\* goto end ) REM Check if sphinx-build is available and fallback to Python version if any %SPHINXBUILD% 2> nul if errorlevel 9009 goto sphinx_python goto sphinx_ok :sphinx_python set SPHINXBUILD=python -m sphinx.__init__ %SPHINXBUILD% 2> nul if errorlevel 9009 ( echo. echo.The 'sphinx-build' command was not found. Make sure you have Sphinx echo.installed, then set the SPHINXBUILD environment variable to point echo.to the full path of the 'sphinx-build' executable. Alternatively you echo.may add the Sphinx directory to PATH. echo. echo.If you don't have Sphinx installed, grab it from echo.http://sphinx-doc.org/ exit /b 1 ) :sphinx_ok if "%1" == "html" ( %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html if errorlevel 1 exit /b 1 echo. echo.Build finished. The HTML pages are in %BUILDDIR%/html. goto end ) if "%1" == "dirhtml" ( %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml if errorlevel 1 exit /b 1 echo. echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml. goto end ) if "%1" == "singlehtml" ( %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml if errorlevel 1 exit /b 1 echo. echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml. goto end ) if "%1" == "pickle" ( %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle if errorlevel 1 exit /b 1 echo. echo.Build finished; now you can process the pickle files. goto end ) if "%1" == "json" ( %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json if errorlevel 1 exit /b 1 echo. echo.Build finished; now you can process the JSON files. goto end ) if "%1" == "htmlhelp" ( %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp if errorlevel 1 exit /b 1 echo. echo.Build finished; now you can run HTML Help Workshop with the ^ .hhp project file in %BUILDDIR%/htmlhelp. goto end ) if "%1" == "qthelp" ( %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp if errorlevel 1 exit /b 1 echo. echo.Build finished; now you can run "qcollectiongenerator" with the ^ .qhcp project file in %BUILDDIR%/qthelp, like this: echo.^> qcollectiongenerator %BUILDDIR%\qthelp\LetsEncrypt.qhcp echo.To view the help file: echo.^> assistant -collectionFile %BUILDDIR%\qthelp\LetsEncrypt.ghc goto end ) if "%1" == "devhelp" ( %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp if errorlevel 1 exit /b 1 echo. echo.Build finished. goto end ) if "%1" == "epub" ( %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub if errorlevel 1 exit /b 1 echo. echo.Build finished. The epub file is in %BUILDDIR%/epub. goto end ) if "%1" == "latex" ( %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex if errorlevel 1 exit /b 1 echo. echo.Build finished; the LaTeX files are in %BUILDDIR%/latex. goto end ) if "%1" == "latexpdf" ( %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex cd %BUILDDIR%/latex make all-pdf cd %~dp0 echo. echo.Build finished; the PDF files are in %BUILDDIR%/latex. goto end ) if "%1" == "latexpdfja" ( %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex cd %BUILDDIR%/latex make all-pdf-ja cd %~dp0 echo. echo.Build finished; the PDF files are in %BUILDDIR%/latex. goto end ) if "%1" == "text" ( %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text if errorlevel 1 exit /b 1 echo. echo.Build finished. The text files are in %BUILDDIR%/text. goto end ) if "%1" == "man" ( %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man if errorlevel 1 exit /b 1 echo. echo.Build finished. The manual pages are in %BUILDDIR%/man. goto end ) if "%1" == "texinfo" ( %SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo if errorlevel 1 exit /b 1 echo. echo.Build finished. The Texinfo files are in %BUILDDIR%/texinfo. goto end ) if "%1" == "gettext" ( %SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/locale if errorlevel 1 exit /b 1 echo. echo.Build finished. The message catalogs are in %BUILDDIR%/locale. goto end ) if "%1" == "changes" ( %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes if errorlevel 1 exit /b 1 echo. echo.The overview file is in %BUILDDIR%/changes. goto end ) if "%1" == "linkcheck" ( %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck if errorlevel 1 exit /b 1 echo. echo.Link check complete; look for any errors in the above output ^ or in %BUILDDIR%/linkcheck/output.txt. goto end ) if "%1" == "doctest" ( %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest if errorlevel 1 exit /b 1 echo. echo.Testing of doctests in the sources finished, look at the ^ results in %BUILDDIR%/doctest/output.txt. goto end ) if "%1" == "coverage" ( %SPHINXBUILD% -b coverage %ALLSPHINXOPTS% %BUILDDIR%/coverage if errorlevel 1 exit /b 1 echo. echo.Testing of coverage in the sources finished, look at the ^ results in %BUILDDIR%/coverage/python.txt. goto end ) if "%1" == "xml" ( %SPHINXBUILD% -b xml %ALLSPHINXOPTS% %BUILDDIR%/xml if errorlevel 1 exit /b 1 echo. echo.Build finished. The XML files are in %BUILDDIR%/xml. goto end ) if "%1" == "pseudoxml" ( %SPHINXBUILD% -b pseudoxml %ALLSPHINXOPTS% %BUILDDIR%/pseudoxml if errorlevel 1 exit /b 1 echo. echo.Build finished. The pseudo-XML files are in %BUILDDIR%/pseudoxml. goto end ) :end letsencrypt-0.4.1/docs/ciphers.rst0000644000175000017500000002247712665157707016637 0ustar bmwbmw00000000000000============ Ciphersuites ============ .. contents:: Table of Contents :local: .. _ciphersuites: Introduction ============ Autoupdates ----------- Within certain limits, TLS server software can choose what kind of cryptography to use when a client connects. These choices can affect security, compatibility, and performance in complex ways. Most of these options are independent of a particular certificate. The Let's Encrypt client tries to provide defaults that we think are most useful to our users. As described below, the Let's Encrypt client will default to modifying server software's cryptographic settings to keep these up-to-date with what we think are appropriate defaults when new versions of the Let's Encrypt client are installed (for example, by an operating system package manager). When this feature is implemented, this document will be updated to describe how to disable these automatic changes. Cryptographic choices --------------------- Software that uses cryptography must inevitably make choices about what kind of cryptography to use and how. These choices entail assumptions about how well particular cryptographic mechanisms resist attack, and what trade-offs are available and appropriate. The choices are constrained by compatibility issues (in order to interoperate with other software, an implementation must agree to use cryptographic mechanisms that the other side also supports) and protocol issues (cryptographic mechanisms must be specified in protocols and there must be a way to agree to use them in a particular context). The best choices for a particular application may change over time in response to new research, new standardization events, changes in computer hardware, and changes in the prevalence of legacy software. Much important research on cryptanalysis and cryptographic vulnerabilities is unpublished because many researchers have been working in the interest of improving some entities' communications security while weakening, or failing to improve, others' security. But important information that improves our understanding of the state of the art is published regularly. When enabling TLS support in a compatible web server (which is a separate step from obtaining a certificate), Let's Encrypt has the ability to update that web server's TLS configuration. Again, this is *different from the cryptographic particulars of the certificate itself*; the certificate as of the initial release will be RSA-signed using one of Let's Encrypt's 2048-bit RSA keys, and will describe the subscriber's RSA public key ("subject public key") of at least 2048 bits, which is used for key establishment. Note that the subscriber's RSA public key can be used in a wide variety of key establishment methods, most of which do not use RSA directly for key exchange, but only for authenticating the server! For example, in DHE and ECDHE key exchanges, the subject public key is just used to sign other parameters for authentication. You do not have to "use RSA" for other purposes just because you're using an RSA key for authentication. The certificate doesn't specify other cryptographic or ciphersuite particulars; for example, it doesn't say whether or not parties should use a particular symmetric algorithm like 3DES, or what cipher modes they should use. All of these details are negotiated between client and server independent of the content of the ciphersuite. The Let's Encrypt project hopes to provide useful defaults that reflect good security choices with respect to the publicly-known state of the art. However, the Let's Encrypt certificate authority does *not* dictate end-users' security policy, and any site is welcome to change its preferences in accordance with its own policy or its administrators' preferences, and use different cryptographic mechanisms or parameters, or a different priority order, than the defaults provided by the Let's Encrypt client. If you don't use the Let's Encrypt client to configure your server directly, because the client doesn't integrate with your server software or because you chose not to use this integration, then the cryptographic defaults haven't been modified, and the cryptography chosen by the server will still be whatever the default for your software was. For example, if you obtain a certificate using *standalone* mode and then manually install it in an IMAP or LDAP server, your cryptographic settings will not be modified by the client in any way. Sources of defaults ------------------- Initially, the Let's Encrypt client will configure users' servers to use the cryptographic defaults recommended by the Mozilla project. These settings are well-reasoned recommendations that carefully consider client software compatibility. They are described at https://wiki.mozilla.org/Security/Server_Side_TLS and the version implemented by the Let's Encrypt client will be the version that was most current as of the release date of each client version. Mozilla offers three separate sets of cryptographic options, which trade off security and compatibility differently. These are referred to as the "Modern", "Intermediate", and "Old" configurations (in order from most secure to least secure, and least-backwards compatible to most-backwards compatible). The client will follow the Mozilla defaults for the *Intermediate* configuration by default, at least with regards to ciphersuites and TLS versions. Mozilla's web site describes which client software will be compatible with each configuration. You can also use the Qualys SSL Labs site, which the Let's Encrypt software will suggest when installing a certificate, to test your server and see whether it will be compatible with particular software versions. It will be possible to ask the Let's Encrypt client to instead apply (and track) Modern or Old configurations. The Let's Encrypt project expects to follow the Mozilla recommendations in the future as those recommendations are updated. (For example, some users have proposed prioritizing a new ciphersuite known as ``0xcc13`` which uses the ChaCha and Poly1305 algorithms, and which is already implemented by the Chrome browser. Mozilla has delayed recommending ``0xcc13`` over compatibility and standardization concerns, but is likely to recommend it in the future once these concerns have been addressed. At that point, the Let's Encrypt client would likely follow the Mozilla recommendations and favor the use of this ciphersuite as well.) The Let's Encrypt project may deviate from the Mozilla recommendations in the future if good cause is shown and we believe our users' priorities would be well-served by doing so. In general, please address relevant proposals for changing priorities to the Mozilla security team first, before asking the Let's Encrypt project to change the client's priorities. The Mozilla security team is likely to have more resources and expertise to bring to bear on evaluating reasons why its recommendations should be updated. The Let's Encrypt project will entertain proposals to create a *very* small number of alternative configurations (apart from Modern, Intermediate, and Old) that there's reason to believe would be widely used by sysadmins; this would usually be a preferable course to modifying an existing configuration. For example, if many sysadmins want their servers configured to track a different expert recommendation, Let's Encrypt could add an option to do so. Resources for recommendations ----------------------------- In the course of considering how to handle this issue, we received recommendations with sources of expert guidance on ciphersuites and other cryptographic parameters. We're grateful to everyone who contributed suggestions. The recommendations we received are available at https://github.com/letsencrypt/letsencrypt/wiki/Ciphersuite-guidance Let's Encrypt client users are welcome to review these authorities to better inform their own cryptographic parameter choices. We also welcome suggestions of other resources to add to this list. Please keep in mind that different recommendations may reflect different priorities or evaluations of trade-offs, especially related to compatibility! Changing your settings ---------------------- This will probably look something like .. code-block:: shell letsencrypt --cipher-recommendations mozilla-secure letsencrypt --cipher-recommendations mozilla-intermediate letsencrypt --cipher-recommendations mozilla-old to track Mozilla's *Secure*, *Intermediate*, or *Old* recommendations, and .. code-block:: shell letsencrypt --update-ciphers on to enable updating ciphers with each new Let's Encrypt client release, or .. code-block:: shell letsencrypt --update-ciphers off to disable automatic configuration updates. These features have not yet been implemented and this syntax may change then they are implemented. TODO ---- The status of this feature is tracked as part of issue #1123 in our bug tracker. https://github.com/letsencrypt/letsencrypt/issues/1123 Prior to implementation of #1123, the client does not actually modify ciphersuites (this is intended to be implemented as a "configuration enhancement", but the only configuration enhancement implemented so far is redirecting HTTP requests to HTTPS in web servers, the "redirect" enhancement). The changes here would probably be either a new "ciphersuite" enhancement in each plugin that provides an installer, or a family of enhancements, one per selectable ciphersuite configuration. letsencrypt-0.4.1/docs/Makefile0000644000175000017500000001563412665157707016105 0ustar bmwbmw00000000000000# Makefile for Sphinx documentation # # You can set these variables from the command line. SPHINXOPTS = SPHINXBUILD = sphinx-build PAPER = BUILDDIR = _build # User-friendly check for sphinx-build ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1) $(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don't have Sphinx installed, grab it from http://sphinx-doc.org/) endif # Internal variables. PAPEROPT_a4 = -D latex_paper_size=a4 PAPEROPT_letter = -D latex_paper_size=letter ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . # the i18n builder cannot share the environment and doctrees with the others I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest coverage gettext help: @echo "Please use \`make ' where is one of" @echo " html to make standalone HTML files" @echo " dirhtml to make HTML files named index.html in directories" @echo " singlehtml to make a single large HTML file" @echo " pickle to make pickle files" @echo " json to make JSON files" @echo " htmlhelp to make HTML files and a HTML help project" @echo " qthelp to make HTML files and a qthelp project" @echo " devhelp to make HTML files and a Devhelp project" @echo " epub to make an epub" @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" @echo " latexpdf to make LaTeX files and run them through pdflatex" @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx" @echo " text to make text files" @echo " man to make manual pages" @echo " texinfo to make Texinfo files" @echo " info to make Texinfo files and run them through makeinfo" @echo " gettext to make PO message catalogs" @echo " changes to make an overview of all changed/added/deprecated items" @echo " xml to make Docutils-native XML files" @echo " pseudoxml to make pseudoxml-XML files for display purposes" @echo " linkcheck to check all external links for integrity" @echo " doctest to run all doctests embedded in the documentation (if enabled)" @echo " coverage to run coverage check of the documentation (if enabled)" clean: rm -rf $(BUILDDIR)/* html: $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." dirhtml: $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." singlehtml: $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml @echo @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." pickle: $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle @echo @echo "Build finished; now you can process the pickle files." json: $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json @echo @echo "Build finished; now you can process the JSON files." htmlhelp: $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp @echo @echo "Build finished; now you can run HTML Help Workshop with the" \ ".hhp project file in $(BUILDDIR)/htmlhelp." qthelp: $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp @echo @echo "Build finished; now you can run "qcollectiongenerator" with the" \ ".qhcp project file in $(BUILDDIR)/qthelp, like this:" @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/LetsEncrypt.qhcp" @echo "To view the help file:" @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/LetsEncrypt.qhc" devhelp: $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp @echo @echo "Build finished." @echo "To view the help file:" @echo "# mkdir -p $$HOME/.local/share/devhelp/LetsEncrypt" @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/LetsEncrypt" @echo "# devhelp" epub: $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub @echo @echo "Build finished. The epub file is in $(BUILDDIR)/epub." latex: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." @echo "Run \`make' in that directory to run these through (pdf)latex" \ "(use \`make latexpdf' here to do that automatically)." latexpdf: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo "Running LaTeX files through pdflatex..." $(MAKE) -C $(BUILDDIR)/latex all-pdf @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." latexpdfja: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo "Running LaTeX files through platex and dvipdfmx..." $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." text: $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text @echo @echo "Build finished. The text files are in $(BUILDDIR)/text." man: $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man @echo @echo "Build finished. The manual pages are in $(BUILDDIR)/man." texinfo: $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo @echo @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." @echo "Run \`make' in that directory to run these through makeinfo" \ "(use \`make info' here to do that automatically)." info: $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo @echo "Running Texinfo files through makeinfo..." make -C $(BUILDDIR)/texinfo info @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." gettext: $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale @echo @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." changes: $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes @echo @echo "The overview file is in $(BUILDDIR)/changes." linkcheck: $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck @echo @echo "Link check complete; look for any errors in the above output " \ "or in $(BUILDDIR)/linkcheck/output.txt." doctest: $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest @echo "Testing of doctests in the sources finished, look at the " \ "results in $(BUILDDIR)/doctest/output.txt." coverage: $(SPHINXBUILD) -b coverage $(ALLSPHINXOPTS) $(BUILDDIR)/coverage @echo "Testing of coverage in the sources finished, look at the " \ "results in $(BUILDDIR)/coverage/python.txt." xml: $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml @echo @echo "Build finished. The XML files are in $(BUILDDIR)/xml." pseudoxml: $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml @echo @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml." letsencrypt-0.4.1/docs/api/0000755000175000017500000000000012665157717015206 5ustar bmwbmw00000000000000letsencrypt-0.4.1/docs/api/crypto_util.rst0000644000175000017500000000016412665157707020315 0ustar bmwbmw00000000000000:mod:`letsencrypt.crypto_util` ------------------------------ .. automodule:: letsencrypt.crypto_util :members: letsencrypt-0.4.1/docs/api/storage.rst0000644000175000017500000000015012665157707017377 0ustar bmwbmw00000000000000:mod:`letsencrypt.storage` -------------------------- .. automodule:: letsencrypt.storage :members: letsencrypt-0.4.1/docs/api/constants.rst0000644000175000017500000000016512665157707017755 0ustar bmwbmw00000000000000:mod:`letsencrypt.constants` ----------------------------------- .. automodule:: letsencrypt.constants :members: letsencrypt-0.4.1/docs/api/reporter.rst0000644000175000017500000000015312665157707017600 0ustar bmwbmw00000000000000:mod:`letsencrypt.reporter` --------------------------- .. automodule:: letsencrypt.reporter :members: letsencrypt-0.4.1/docs/api/auth_handler.rst0000644000175000017500000000016712665157707020401 0ustar bmwbmw00000000000000:mod:`letsencrypt.auth_handler` ------------------------------- .. automodule:: letsencrypt.auth_handler :members: letsencrypt-0.4.1/docs/api/display.rst0000644000175000017500000000074512665157707017412 0ustar bmwbmw00000000000000:mod:`letsencrypt.display` -------------------------- .. automodule:: letsencrypt.display :members: :mod:`letsencrypt.display.util` =============================== .. automodule:: letsencrypt.display.util :members: :mod:`letsencrypt.display.ops` ============================== .. automodule:: letsencrypt.display.ops :members: :mod:`letsencrypt.display.enhancements` ======================================= .. automodule:: letsencrypt.display.enhancements :members: letsencrypt-0.4.1/docs/api/configuration.rst0000644000175000017500000000017212665157707020606 0ustar bmwbmw00000000000000:mod:`letsencrypt.configuration` -------------------------------- .. automodule:: letsencrypt.configuration :members: letsencrypt-0.4.1/docs/api/account.rst0000644000175000017500000000015012665157707017367 0ustar bmwbmw00000000000000:mod:`letsencrypt.account` -------------------------- .. automodule:: letsencrypt.account :members: letsencrypt-0.4.1/docs/api/proof_of_possession.rst0000644000175000017500000000021412665157707022032 0ustar bmwbmw00000000000000:mod:`letsencrypt.proof_of_possession` -------------------------------------- .. automodule:: letsencrypt.proof_of_possession :members: letsencrypt-0.4.1/docs/api/log.rst0000644000175000017500000000013412665157707016516 0ustar bmwbmw00000000000000:mod:`letsencrypt.log` ---------------------- .. automodule:: letsencrypt.log :members: letsencrypt-0.4.1/docs/api/reverter.rst0000644000175000017500000000015312665157707017574 0ustar bmwbmw00000000000000:mod:`letsencrypt.reverter` --------------------------- .. automodule:: letsencrypt.reverter :members: letsencrypt-0.4.1/docs/api/index.rst0000644000175000017500000000012012665157707017037 0ustar bmwbmw00000000000000:mod:`letsencrypt` ------------------ .. automodule:: letsencrypt :members: letsencrypt-0.4.1/docs/api/le_util.rst0000644000175000017500000000015012665157707017370 0ustar bmwbmw00000000000000:mod:`letsencrypt.le_util` -------------------------- .. automodule:: letsencrypt.le_util :members: letsencrypt-0.4.1/docs/api/client.rst0000644000175000017500000000014512665157707017215 0ustar bmwbmw00000000000000:mod:`letsencrypt.client` ------------------------- .. automodule:: letsencrypt.client :members: letsencrypt-0.4.1/docs/api/achallenges.rst0000644000175000017500000000016412665157707020206 0ustar bmwbmw00000000000000:mod:`letsencrypt.achallenges` ------------------------------ .. automodule:: letsencrypt.achallenges :members: letsencrypt-0.4.1/docs/api/plugins/0000755000175000017500000000000012665157717016667 5ustar bmwbmw00000000000000letsencrypt-0.4.1/docs/api/plugins/webroot.rst0000644000175000017500000000020012665157707021071 0ustar bmwbmw00000000000000:mod:`letsencrypt.plugins.webroot` ---------------------------------- .. automodule:: letsencrypt.plugins.webroot :members: letsencrypt-0.4.1/docs/api/plugins/common.rst0000644000175000017500000000017512665157707020713 0ustar bmwbmw00000000000000:mod:`letsencrypt.plugins.common` --------------------------------- .. automodule:: letsencrypt.plugins.common :members: letsencrypt-0.4.1/docs/api/plugins/standalone.rst0000644000175000017500000000021112665157707021542 0ustar bmwbmw00000000000000:mod:`letsencrypt.plugins.standalone` ------------------------------------- .. automodule:: letsencrypt.plugins.standalone :members: letsencrypt-0.4.1/docs/api/plugins/disco.rst0000644000175000017500000000017212665157707020521 0ustar bmwbmw00000000000000:mod:`letsencrypt.plugins.disco` -------------------------------- .. automodule:: letsencrypt.plugins.disco :members: letsencrypt-0.4.1/docs/api/plugins/util.rst0000644000175000017500000000016712665157707020401 0ustar bmwbmw00000000000000:mod:`letsencrypt.plugins.util` ------------------------------- .. automodule:: letsencrypt.plugins.util :members: letsencrypt-0.4.1/docs/api/plugins/manual.rst0000644000175000017500000000017512665157707020700 0ustar bmwbmw00000000000000:mod:`letsencrypt.plugins.manual` --------------------------------- .. automodule:: letsencrypt.plugins.manual :members: letsencrypt-0.4.1/docs/api/interfaces.rst0000644000175000017500000000016112665157707020060 0ustar bmwbmw00000000000000:mod:`letsencrypt.interfaces` ----------------------------- .. automodule:: letsencrypt.interfaces :members: letsencrypt-0.4.1/docs/api/continuity_auth.rst0000644000175000017500000000020012665157707021155 0ustar bmwbmw00000000000000:mod:`letsencrypt.continuity_auth` ---------------------------------- .. automodule:: letsencrypt.continuity_auth :members: letsencrypt-0.4.1/docs/api/errors.rst0000644000175000017500000000014512665157707017253 0ustar bmwbmw00000000000000:mod:`letsencrypt.errors` ------------------------- .. automodule:: letsencrypt.errors :members: letsencrypt-0.4.1/docs/conf.py0000644000175000017500000002450412665157707015740 0ustar bmwbmw00000000000000# -*- coding: utf-8 -*- # # Let's Encrypt documentation build configuration file, created by # sphinx-quickstart on Sun Nov 23 20:35:21 2014. # # 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. import codecs import os import re import sys here = os.path.abspath(os.path.dirname(__file__)) # read version number (and other metadata) from package init init_fn = os.path.join(here, '..', 'letsencrypt', '__init__.py') with codecs.open(init_fn, encoding='utf8') as fd: meta = dict(re.findall(r"""__([a-z]+)__ = '([^']+)""", fd.read())) # 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. sys.path.insert(0, os.path.abspath(os.path.join(here, '..'))) # -- 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.todo', 'sphinx.ext.coverage', 'sphinx.ext.viewcode', 'repoze.sphinx.autointerface', 'sphinxcontrib.programoutput', ] autodoc_member_order = 'bysource' autodoc_default_flags = ['show-inheritance', 'private-members'] # Add any paths that contain templates here, relative to this directory. templates_path = ['_templates'] # The suffix of source filenames. source_suffix = '.rst' # The encoding of source files. #source_encoding = 'utf-8-sig' # The master toctree document. master_doc = 'index' # General information about the project. project = u'Let\'s Encrypt' copyright = u'2014-2015, Let\'s Encrypt Project' # 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. # # The short X.Y version. version = '.'.join(meta['version'].split('.')[:2]) # The full version, including alpha/beta/rc tags. release = meta['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 # There are two options for replacing |today|: either, you set today to some # non-false value, then it is used: #today = '' # Else, today_fmt is used as the format for a strftime call. #today_fmt = '%B %d, %Y' # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. exclude_patterns = ['_build'] # The reST default role (used for this markup: `text`) to use for all # documents. default_role = 'py:obj' # If true, '()' will be appended to :func: etc. cross-reference text. #add_function_parentheses = True # If true, the current module name will be prepended to all description # unit titles (such as .. function::). #add_module_names = True # If true, sectionauthor and moduleauthor directives will be shown in the # output. They are ignored by default. #show_authors = False # The name of the Pygments (syntax highlighting) style to use. pygments_style = 'sphinx' # A list of ignored prefixes for module index sorting. #modindex_common_prefix = [] # If true, keep warnings as "system message" paragraphs in the built documents. #keep_warnings = False # If true, `todo` and `todoList` produce output, else they produce nothing. todo_include_todos = True # -- Options for HTML output ---------------------------------------------- # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. # http://docs.readthedocs.org/en/latest/theme.html#how-do-i-use-this-locally-and-on-read-the-docs # on_rtd is whether we are on readthedocs.org on_rtd = os.environ.get('READTHEDOCS', None) == 'True' if not on_rtd: # only import and set the theme if we're building docs locally import sphinx_rtd_theme html_theme = 'sphinx_rtd_theme' html_theme_path = [sphinx_rtd_theme.get_html_theme_path()] # otherwise, readthedocs.org uses their theme by default, so no need to specify it # 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 themes here, relative to this directory. #html_theme_path = [] # The name for this set of Sphinx documents. If None, it defaults to # " v documentation". #html_title = None # A shorter title for the navigation bar. Default is the same as html_title. #html_short_title = None # The name of an image file (relative to this directory) to place at the top # of the sidebar. #html_logo = None # The name of an image file (within the static path) to use as favicon of the # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 # pixels large. #html_favicon = None # 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'] # Add any extra paths that contain custom files (such as robots.txt or # .htaccess) here, relative to this directory. These files are copied # directly to the root of the documentation. #html_extra_path = [] # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, # using the given strftime format. #html_last_updated_fmt = '%b %d, %Y' # If true, SmartyPants will be used to convert quotes and dashes to # typographically correct entities. #html_use_smartypants = True # Custom sidebar templates, maps document names to template names. #html_sidebars = {} # Additional templates that should be rendered to pages, maps page names to # template names. #html_additional_pages = {} # If false, no module index is generated. #html_domain_indices = True # If false, no index is generated. #html_use_index = True # If true, the index is split into individual pages for each letter. #html_split_index = False # If true, links to the reST sources are added to the pages. #html_show_sourcelink = True # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. #html_show_sphinx = True # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. #html_show_copyright = True # If true, an OpenSearch description file will be output, and all pages will # contain a tag referring to it. The value of this option must be the # base URL from which the finished HTML is served. #html_use_opensearch = '' # This is the file name suffix for HTML files (e.g. ".xhtml"). #html_file_suffix = None # Language to be used for generating the HTML full-text search index. # Sphinx supports the following languages: # 'da', 'de', 'en', 'es', 'fi', 'fr', 'hu', 'it', 'ja' # 'nl', 'no', 'pt', 'ro', 'ru', 'sv', 'tr' #html_search_language = 'en' # A dictionary with options for the search language support, empty by default. # Now only 'ja' uses this config value #html_search_options = {'type': 'default'} # The name of a javascript file (relative to the configuration directory) that # implements a search results scorer. If empty, the default will be used. #html_search_scorer = 'scorer.js' # Output file base name for HTML help builder. htmlhelp_basename = 'LetsEncryptdoc' # -- 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 = [ ('index', 'LetsEncrypt.tex', u'Let\'s Encrypt Documentation', u'Let\'s Encrypt Project', 'manual'), ] # The name of an image file (relative to this directory) to place at the top of # the title page. #latex_logo = None # For "manual" documents, if this is true, then toplevel headings are parts, # not chapters. #latex_use_parts = False # If true, show page references after internal links. #latex_show_pagerefs = False # If true, show URL addresses after external links. #latex_show_urls = False # Documents to append as an appendix to all manuals. #latex_appendices = [] # If false, no module index is generated. #latex_domain_indices = True # -- Options for manual page output --------------------------------------- # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). man_pages = [ ('index', 'letsencrypt', u'Let\'s Encrypt Documentation', [project], 7), ('man/letsencrypt', 'letsencrypt', u'letsencrypt script documentation', [project], 1), ] # If true, show URL addresses after external links. #man_show_urls = False # -- 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 = [ ('index', 'LetsEncrypt', u'Let\'s Encrypt Documentation', u'Let\'s Encrypt Project', 'LetsEncrypt', 'One line description of project.', 'Miscellaneous'), ] # Documents to append as an appendix to all manuals. #texinfo_appendices = [] # If false, no module index is generated. #texinfo_domain_indices = True # How to display URL addresses: 'footnote', 'no', or 'inline'. #texinfo_show_urls = 'footnote' # If true, do not generate a @detailmenu in the "Top" node's menu. #texinfo_no_detailmenu = False intersphinx_mapping = { 'python': ('https://docs.python.org/', None), 'acme': ('https://acme-python.readthedocs.org/en/latest/', None), } letsencrypt-0.4.1/docs/intro.rst0000644000175000017500000000013712665157707016322 0ustar bmwbmw00000000000000============ Introduction ============ .. include:: ../README.rst .. include:: ../CHANGES.rst letsencrypt-0.4.1/docs/index.rst0000644000175000017500000000046712665157707016304 0ustar bmwbmw00000000000000Welcome to the Let's Encrypt client documentation! ================================================== .. toctree:: :maxdepth: 2 intro using contributing packaging .. toctree:: :maxdepth: 1 api Indices and tables ================== * :ref:`genindex` * :ref:`modindex` * :ref:`search` letsencrypt-0.4.1/docs/using.rst0000644000175000017500000004646212665157707016327 0ustar bmwbmw00000000000000========== User Guide ========== .. contents:: Table of Contents :local: .. _installation: Installation ============ .. _letsencrypt-auto: letsencrypt-auto ---------------- ``letsencrypt-auto`` is a wrapper which installs some dependencies from your OS standard package repositories (e.g. using `apt-get` or `yum`), and for other dependencies it sets up a virtualized Python environment with packages downloaded from PyPI [#venv]_. It also provides automated updates. To install and run the client, just type... .. code-block:: shell ./letsencrypt-auto .. hint:: During the beta phase, Let's Encrypt enforces strict rate limits on the number of certificates issued for one domain. It is recommended to initially use the test server via `--test-cert` until you get the desired certificates. Throughout the documentation, whenever you see references to ``letsencrypt`` script/binary, you can substitute in ``letsencrypt-auto``. For example, to get basic help you would type: .. code-block:: shell ./letsencrypt-auto --help or for full help, type: .. code-block:: shell ./letsencrypt-auto --help all ``letsencrypt-auto`` is the recommended method of running the Let's Encrypt client beta releases on systems that don't have a packaged version. Debian, Arch Linux, Gentoo, FreeBSD, and OpenBSD now have native packages, so on those systems you can just install ``letsencrypt`` (and perhaps ``letsencrypt-apache``). If you'd like to run the latest copy from Git, or run your own locally modified copy of the client, follow the instructions in the :doc:`contributing`. Some `other methods of installation`_ are discussed below. Plugins ======= The Let's Encrypt client supports a number of different "plugins" that can be used to obtain and/or install certificates. Plugins that can obtain a cert are called "authenticators" and can be used with the "certonly" command. Plugins that can install a cert are called "installers". Plugins that do both can be used with the "letsencrypt run" command, which is the default. =========== ==== ==== =============================================================== Plugin Auth Inst Notes =========== ==== ==== =============================================================== apache_ Y Y Automates obtaining and installing a cert with Apache 2.4 on Debian-based distributions with ``libaugeas0`` 1.0+. standalone_ Y N Uses a "standalone" webserver to obtain a cert. This is useful on systems with no webserver, or when direct integration with the local webserver is not supported or not desired. webroot_ Y N Obtains a cert by writing to the webroot directory of an already running webserver. manual_ Y N Helps you obtain a cert by giving you instructions to perform domain validation yourself. nginx_ Y Y Very experimental and not included in letsencrypt-auto_. =========== ==== ==== =============================================================== Future plugins for IMAP servers, SMTP servers, IRC servers, etc, are likely to be installers but not authenticators. Apache ------ If you're running Apache 2.4 on a Debian-based OS with version 1.0+ of the ``libaugeas0`` package available, you can use the Apache plugin. This automates both obtaining *and* installing certs on an Apache webserver. To specify this plugin on the command line, simply include ``--apache``. Webroot ------- If you're running a local webserver for which you have the ability to modify the content being served, and you'd prefer not to stop the webserver during the certificate issuance process, you can use the webroot plugin to obtain a cert by including ``certonly`` and ``--webroot`` on the command line. In addition, you'll need to specify ``--webroot-path`` or ``-w`` with the top-level directory ("web root") containing the files served by your webserver. For example, ``--webroot-path /var/www/html`` or ``--webroot-path /usr/share/nginx/html`` are two common webroot paths. If you're getting a certificate for many domains at once, the plugin needs to know where each domain's files are served from, which could potentially be a separate directory for each domain. When requested a certificate for multiple domains, each domain will use the most recently specified ``--webroot-path``. So, for instance, ``letsencrypt certonly --webroot -w /var/www/example/ -d www.example.com -d example.com -w /var/www/other -d other.example.net -d another.other.example.net`` would obtain a single certificate for all of those names, using the ``/var/www/example`` webroot directory for the first two, and ``/var/www/other`` for the second two. The webroot plugin works by creating a temporary file for each of your requested domains in ``${webroot-path}/.well-known/acme-challenge``. Then the Let's Encrypt validation server makes HTTP requests to validate that the DNS for each requested domain resolves to the server running letsencrypt. An example request made to your web server would look like: :: 66.133.109.36 - - [05/Jan/2016:20:11:24 -0500] "GET /.well-known/acme-challenge/HGr8U1IeTW4kY_Z6UIyaakzOkyQgPr_7ArlLgtZE8SX HTTP/1.1" 200 87 "-" "Mozilla/5.0 (compatible; Let's Encrypt validation server; +https://www.letsencrypt.org)" Note that to use the webroot plugin, your server must be configured to serve files from hidden directories. If ``/.well-known`` is treated specially by your webserver configuration, you might need to modify the configuration to ensure that files inside ``/.well-known/acme-challenge`` are served by the webserver. Standalone ---------- To obtain a cert using a "standalone" webserver, you can use the standalone plugin by including ``certonly`` and ``--standalone`` on the command line. This plugin needs to bind to port 80 or 443 in order to perform domain validation, so you may need to stop your existing webserver. To control which port the plugin uses, include one of the options shown below on the command line. * ``--standalone-supported-challenges http-01`` to use port 80 * ``--standalone-supported-challenges tls-sni-01`` to use port 443 The standalone plugin does not rely on any other server software running on the machine where you obtain the certificate. It must still be possible for that machine to accept inbound connections from the Internet on the specified port using each requested domain name. Manual ------ If you'd like to obtain a cert running ``letsencrypt`` on a machine other than your target webserver or perform the steps for domain validation yourself, you can use the manual plugin. While hidden from the UI, you can use the plugin to obtain a cert by specifying ``certonly`` and ``--manual`` on the command line. This requires you to copy and paste commands into another terminal session, which may be on a different computer. Nginx ----- In the future, if you're running Nginx you can use this plugin to automatically obtain and install your certificate. The Nginx plugin is still experimental, however, and is not installed with letsencrypt-auto_. If installed, you can select this plugin on the command line by including ``--nginx``. Third-party plugins ------------------- These plugins are listed at https://github.com/letsencrypt/letsencrypt/wiki/Plugins. If you're interested, you can also :ref:`write your own plugin `. Renewal ======= .. note:: Let's Encrypt CA issues short-lived certificates (90 days). Make sure you renew the certificates at least once in 3 months. The ``letsencrypt`` client now supports a ``renew`` action to check all installed certificates for impending expiry and attempt to renew them. The simplest form is simply ``letsencrypt renew`` This will attempt to renew any previously-obtained certificates that expire in less than 30 days. The same plugin and options that were used at the time the certificate was originally issued will be used for the renewal attempt, unless you specify other plugins or options. If you're sure that this command executes successfully without human intervention, you can add the command to ``crontab`` (since certificates are only renewed when they're determined to be near expiry, the command can run on a regular basis, like every week or every day); note that the current version provides detailed output describing either renewal success or failure. The ``--force-renew`` flag may be helpful for automating renewal; it causes the expiration time of the certificate(s) to be ignored when considering renewal, and attempts to renew each and every installed certificate regardless of its age. (This form is not appropriate to run daily because each certificate will be renewed every day, which will quickly run into the certificate authority rate limit.) Note that options provided to ``letsencrypt renew`` will apply to *every* certificate for which renewal is attempted; for example, ``letsencrypt renew --rsa-key-size 4096`` would try to replace every near-expiry certificate with an equivalent certificate using a 4096-bit RSA public key. If a certificate is successfully renewed using specified options, those options will be saved and used for future renewals of that certificate. An alternative form that provides for more fine-grained control over the renewal process (while renewing specified certificates one at a time), is ``letsencrypt certonly`` with the complete set of subject domains of a specific certificate specified via `-d` flags, like ``letsencrypt certonly -d example.com -d www.example.com`` (All of the domains covered by the certificate must be specified in this case in order to renew and replace the old certificate rather than obtaining a new one; don't forget any `www.` domains! Specifying a subset of the domains creates a new, separate certificate containing only those domains, rather than replacing the original certificate.) The ``certonly`` form attempts to renew one individual certificate. Please note that the CA will send notification emails to the address you provide if you do not renew certificates that are about to expire. Let's Encrypt is working hard on improving the renewal process, and we apologize for any inconveniences you encounter in integrating these commands into your individual environment. .. _where-certs: Where are my certificates? ========================== First of all, we encourage you to use Apache or nginx installers, both which perform the certificate management automatically. If, however, you prefer to manage everything by hand, this section provides information on where to find necessary files. All generated keys and issued certificates can be found in ``/etc/letsencrypt/live/$domain``. Rather than copying, please point your (web) server configuration directly to those files (or create symlinks). During the renewal_, ``/etc/letsencrypt/live`` is updated with the latest necessary files. .. note:: ``/etc/letsencrypt/archive`` and ``/etc/letsencrypt/keys`` contain all previous keys and certificates, while ``/etc/letsencrypt/live`` symlinks to the latest versions. The following files are available: ``privkey.pem`` Private key for the certificate. .. warning:: This **must be kept secret at all times**! Never share it with anyone, including Let's Encrypt developers. You cannot put it into a safe, however - your server still needs to access this file in order for SSL/TLS to work. This is what Apache needs for `SSLCertificateKeyFile `_, and nginx for `ssl_certificate_key `_. ``cert.pem`` Server certificate only. This is what Apache < 2.4.8 needs for `SSLCertificateFile `_. ``chain.pem`` All certificates that need to be served by the browser **excluding** server certificate, i.e. root and intermediate certificates only. This is what Apache < 2.4.8 needs for `SSLCertificateChainFile `_, and what nginx >= 1.3.7 needs for `ssl_trusted_certificate `_. ``fullchain.pem`` All certificates, **including** server certificate. This is concatenation of ``chain.pem`` and ``cert.pem``. This is what Apache >= 2.4.8 needs for `SSLCertificateFile `_, and what nginx needs for `ssl_certificate `_. For both chain files, all certificates are ordered from root (primary certificate) towards leaf. Please note, that **you must use** either ``chain.pem`` or ``fullchain.pem``. In case of webservers, using only ``cert.pem``, will cause nasty errors served through the browsers! .. note:: All files are PEM-encoded (as the filename suffix suggests). If you need other format, such as DER or PFX, then you could convert using ``openssl``, but this means you will not benefit from automatic renewal_! .. _config-file: Configuration file ================== It is possible to specify configuration file with ``letsencrypt-auto --config cli.ini`` (or shorter ``-c cli.ini``). An example configuration file is shown below: .. include:: ../examples/cli.ini :code: ini By default, the following locations are searched: - ``/etc/letsencrypt/cli.ini`` - ``$XDG_CONFIG_HOME/letsencrypt/cli.ini`` (or ``~/.config/letsencrypt/cli.ini`` if ``$XDG_CONFIG_HOME`` is not set). .. keep it up to date with constants.py Getting help ============ If you're having problems you can chat with us on `IRC (#letsencrypt @ Freenode) `_ or get support on our `forums `_. If you find a bug in the software, please do report it in our `issue tracker `_. Remember to give us as much information as possible: - copy and paste exact command line used and the output (though mind that the latter might include some personally identifiable information, including your email and domains) - copy and paste logs from ``/var/log/letsencrypt`` (though mind they also might contain personally identifiable information) - copy and paste ``letsencrypt --version`` output - your operating system, including specific version - specify which installation_ method you've chosen Other methods of installation ============================= Running with Docker ------------------- Docker_ is an amazingly simple and quick way to obtain a certificate. However, this mode of operation is unable to install certificates or configure your webserver, because our installer plugins cannot reach it from inside the Docker container. You should definitely read the :ref:`where-certs` section, in order to know how to manage the certs manually. https://github.com/letsencrypt/letsencrypt/wiki/Ciphersuite-guidance provides some information about recommended ciphersuites. If none of these make much sense to you, you should definitely use the letsencrypt-auto_ method, which enables you to use installer plugins that cover both of those hard topics. If you're still not convinced and have decided to use this method, from the server that the domain you're requesting a cert for resolves to, `install Docker`_, then issue the following command: .. code-block:: shell sudo docker run -it --rm -p 443:443 -p 80:80 --name letsencrypt \ -v "/etc/letsencrypt:/etc/letsencrypt" \ -v "/var/lib/letsencrypt:/var/lib/letsencrypt" \ quay.io/letsencrypt/letsencrypt:latest auth and follow the instructions (note that ``auth`` command is explicitly used - no installer plugins involved). Your new cert will be available in ``/etc/letsencrypt/live`` on the host. .. _Docker: https://docker.com .. _`install Docker`: https://docs.docker.com/userguide/ Operating System Packages -------------------------- **FreeBSD** * Port: ``cd /usr/ports/security/py-letsencrypt && make install clean`` * Package: ``pkg install py27-letsencrypt`` **OpenBSD** * Port: ``cd /usr/ports/security/letsencrypt/client && make install clean`` * Package: ``pkg_add letsencrypt`` **Arch Linux** .. code-block:: shell sudo pacman -S letsencrypt letsencrypt-apache **Debian** If you run Debian Stretch or Debian Sid, you can install letsencrypt packages. .. code-block:: shell sudo apt-get update sudo apt-get install letsencrypt python-letsencrypt-apache If you don't want to use the Apache plugin, you can omit the ``python-letsencrypt-apache`` package. Packages for Debian Jessie are coming in the next few weeks. **Fedora** .. code-block:: shell sudo dnf install letsencrypt **Gentoo** The official Let's Encrypt client is available in Gentoo Portage. If you want to use the Apache plugin, it has to be installed separately: .. code-block:: shell emerge -av app-crypt/letsencrypt emerge -av app-crypt/letsencrypt-apache Currently, only the Apache plugin is included in Portage. However, if you want the nginx plugin, you can use Layman to add the mrueg overlay which does include the nginx plugin package: .. code-block:: shell emerge -av app-portage/layman layman -S layman -a mrueg emerge -av app-crypt/letsencrypt-nginx When using the Apache plugin, you will run into a "cannot find a cert or key directive" error if you're sporting the default Gentoo ``httpd.conf``. You can fix this by commenting out two lines in ``/etc/apache2/httpd.conf`` as follows: Change .. code-block:: shell LoadModule ssl_module modules/mod_ssl.so to .. code-block:: shell # LoadModule ssl_module modules/mod_ssl.so # For the time being, this is the only way for the Apache plugin to recognise the appropriate directives when installing the certificate. Note: this change is not required for the other plugins. **Other Operating Systems** OS packaging is an ongoing effort. If you'd like to package Let's Encrypt client for your distribution of choice please have a look at the :doc:`packaging`. From source ----------- Installation from source is only supported for developers and the whole process is described in the :doc:`contributing`. .. warning:: Please do **not** use ``python setup.py install`` or ``python pip install .``. Please do **not** attempt the installation commands as superuser/root and/or without virtual environment, e.g. ``sudo python setup.py install``, ``sudo pip install``, ``sudo ./venv/bin/...``. These modes of operation might corrupt your operating system and are **not supported** by the Let's Encrypt team! Comparison of different methods ------------------------------- Unless you have a very specific requirements, we kindly ask you to use the letsencrypt-auto_ method. It's the fastest, the most thoroughly tested and the most reliable way of getting our software and the free SSL certificates! Beyond the methods discussed here, other methods may be possible, such as installing Let's Encrypt directly with pip from PyPI or downloading a ZIP archive from GitHub may be technically possible but are not presently recommended or supported. .. rubric:: Footnotes .. [#venv] By using this virtualized Python environment (`virtualenv `_) we don't pollute the main OS space with packages from PyPI! letsencrypt-0.4.1/docs/packaging.rst0000644000175000017500000000021212665157707017105 0ustar bmwbmw00000000000000=============== Packaging Guide =============== Documentation can be found at https://github.com/letsencrypt/letsencrypt/wiki/Packaging. letsencrypt-0.4.1/docs/contributing.rst0000644000175000017500000003437712665157707017713 0ustar bmwbmw00000000000000=============== Developer Guide =============== .. contents:: Table of Contents :local: .. _hacking: Hacking ======= Running a local copy of the client ---------------------------------- Running the client in developer mode from your local tree is a little different than running ``letsencrypt-auto``. To get set up, do these things once: .. code-block:: shell git clone https://github.com/letsencrypt/letsencrypt cd letsencrypt ./letsencrypt-auto-source/letsencrypt-auto --os-packages-only ./tools/venv.sh Then in each shell where you're working on the client, do: .. code-block:: shell source ./venv/bin/activate After that, your shell will be using the virtual environment, and you run the client by typing: .. code-block:: shell letsencrypt Activating a shell in this way makes it easier to run unit tests with ``tox`` and integration tests, as described below. To reverse this, you can type ``deactivate``. More information can be found in the `virtualenv docs`_. .. _`virtualenv docs`: https://virtualenv.pypa.io Find issues to work on ---------------------- You can find the open issues in the `github issue tracker`_. Comparatively easy ones are marked `Good Volunteer Task`_. If you're starting work on something, post a comment to let others know and seek feedback on your plan where appropriate. Once you've got a working branch, you can open a pull request. All changes in your pull request must have thorough unit test coverage, pass our `integration`_ tests, and be compliant with the :ref:`coding style `. .. _github issue tracker: https://github.com/letsencrypt/letsencrypt/issues .. _Good Volunteer Task: https://github.com/letsencrypt/letsencrypt/issues?q=is%3Aopen+is%3Aissue+label%3A%22Good+Volunteer+Task%22 Testing ------- The following tools are there to help you: - ``tox`` starts a full set of tests. Please note that it includes apacheconftest, which uses the system's Apache install to test config file parsing, so it should only be run on systems that have an experimental, non-production Apache2 install on them. ``tox -e apacheconftest`` can be used to run those specific Apache conf tests. - ``tox -e py27``, ``tox -e py26`` etc, run unit tests for specific Python versions. - ``tox -e cover`` checks the test coverage only. Calling the ``./tox.cover.sh`` script directly (or even ``./tox.cover.sh $pkg1 $pkg2 ...`` for any subpackages) might be a bit quicker, though. - ``tox -e lint`` checks the style of the whole project, while ``pylint --rcfile=.pylintrc path`` will check a single file or specific directory only. - For debugging, we recommend ``pip install ipdb`` and putting ``import ipdb; ipdb.set_trace()`` statement inside the source code. Alternatively, you can use Python's standard library `pdb`, but you won't get TAB completion... .. _integration: Integration testing with the boulder CA ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Generally it is sufficient to open a pull request and let Github and Travis run integration tests for you. However, if you prefer to run tests, you can use Vagrant, using the Vagrantfile in Let's Encrypt's repository. To execute the tests on a Vagrant box, the only command you are required to run is:: ./tests/boulder-integration.sh Otherwise, please follow the following instructions. Mac OS X users: Run ``./tests/mac-bootstrap.sh`` instead of ``boulder-start.sh`` to install dependencies, configure the environment, and start boulder. Otherwise, install `Go`_ 1.5, ``libtool-ltdl``, ``mariadb-server`` and ``rabbitmq-server`` and then start Boulder_, an ACME CA server. If you can't get packages of Go 1.5 for your Linux system, you can execute the following commands to install it: .. code-block:: shell wget https://storage.googleapis.com/golang/go1.5.3.linux-amd64.tar.gz -P /tmp/ sudo tar -C /usr/local -xzf /tmp/go1.5.3.linux-amd64.tar.gz if ! grep -Fxq "export GOROOT=/usr/local/go" ~/.profile ; then echo "export GOROOT=/usr/local/go" >> ~/.profile; fi if ! grep -Fxq "export PATH=\\$GOROOT/bin:\\$PATH" ~/.profile ; then echo "export PATH=\\$GOROOT/bin:\\$PATH" >> ~/.profile; fi These commands download `Go`_ 1.5.3 to ``/tmp/``, extracts to ``/usr/local``, and then adds the export lines required to execute ``boulder-start.sh`` to ``~/.profile`` if they were not previously added Make sure you execute the following command after `Go`_ finishes installing:: if ! grep -Fxq "export GOPATH=\\$HOME/go" ~/.profile ; then echo "export GOPATH=\\$HOME/go" >> ~/.profile; fi Afterwards, you'd be able to start Boulder_ using the following command:: ./tests/boulder-start.sh The script will download, compile and run the executable; please be patient - it will take some time... Once its ready, you will see ``Server running, listening on 127.0.0.1:4000...``. Add ``/etc/hosts`` entries pointing ``le.wtf``, ``le1.wtf``, ``le2.wtf``, ``le3.wtf`` and ``nginx.wtf`` to 127.0.0.1. You may now run (in a separate terminal):: ./tests/boulder-integration.sh && echo OK || echo FAIL If you would like to test `letsencrypt_nginx` plugin (highly encouraged) make sure to install prerequisites as listed in ``letsencrypt-nginx/tests/boulder-integration.sh`` and rerun the integration tests suite. .. _Boulder: https://github.com/letsencrypt/boulder .. _Go: https://golang.org Code components and layout ========================== acme contains all protocol specific code letsencrypt all client code Plugin-architecture ------------------- Let's Encrypt has a plugin architecture to facilitate support for different webservers, other TLS servers, and operating systems. The interfaces available for plugins to implement are defined in `interfaces.py`_ and `plugins/common.py`_. The most common kind of plugin is a "Configurator", which is likely to implement the `~letsencrypt.interfaces.IAuthenticator` and `~letsencrypt.interfaces.IInstaller` interfaces (though some Configurators may implement just one of those). There are also `~letsencrypt.interfaces.IDisplay` plugins, which implement bindings to alternative UI libraries. .. _interfaces.py: https://github.com/letsencrypt/letsencrypt/blob/master/letsencrypt/interfaces.py .. _plugins/common.py: https://github.com/letsencrypt/letsencrypt/blob/master/letsencrypt/plugins/common.py#L34 Authenticators -------------- Authenticators are plugins designed to prove that this client deserves a certificate for some domain name by solving challenges received from the ACME server. From the protocol, there are essentially two different types of challenges. Challenges that must be solved by individual plugins in order to satisfy domain validation (subclasses of `~.DVChallenge`, i.e. `~.challenges.TLSSNI01`, `~.challenges.HTTP01`, `~.challenges.DNS`) and continuity specific challenges (subclasses of `~.ContinuityChallenge`, i.e. `~.challenges.RecoveryToken`, `~.challenges.RecoveryContact`, `~.challenges.ProofOfPossession`). Continuity challenges are always handled by the `~.ContinuityAuthenticator`, while plugins are expected to handle `~.DVChallenge` types. Right now, we have two authenticator plugins, the `~.ApacheConfigurator` and the `~.StandaloneAuthenticator`. The Standalone and Apache authenticators only solve the `~.challenges.TLSSNI01` challenge currently. (You can set which challenges your authenticator can handle through the :meth:`~.IAuthenticator.get_chall_pref`. (FYI: We also have a partial implementation for a `~.DNSAuthenticator` in a separate branch). Installer --------- Installers plugins exist to actually setup the certificate in a server, possibly tweak the security configuration to make it more correct and secure (Fix some mixed content problems, turn on HSTS, redirect to HTTPS, etc). Installer plugins tell the main client about their abilities to do the latter via the :meth:`~.IInstaller.supported_enhancements` call. We currently have two Installers in the tree, the `~.ApacheConfigurator`. and the `~.NginxConfigurator`. External projects have made some progress toward support for IIS, Icecast and Plesk. Installers and Authenticators will oftentimes be the same class/object (because for instance both tasks can be performed by a webserver like nginx) though this is not always the case (the standalone plugin is an authenticator that listens on port 443, but it cannot install certs; a postfix plugin would be an installer but not an authenticator). Installers and Authenticators are kept separate because it should be possible to use the `~.StandaloneAuthenticator` (it sets up its own Python server to perform challenges) with a program that cannot solve challenges itself (Such as MTA installers). Installer Development --------------------- There are a few existing classes that may be beneficial while developing a new `~letsencrypt.interfaces.IInstaller`. Installers aimed to reconfigure UNIX servers may use Augeas for configuration parsing and can inherit from `~.AugeasConfigurator` class to handle much of the interface. Installers that are unable to use Augeas may still find the `~.Reverter` class helpful in handling configuration checkpoints and rollback. Display ~~~~~~~ We currently offer a pythondialog and "text" mode for displays. Display plugins implement the `~letsencrypt.interfaces.IDisplay` interface. .. _dev-plugin: Writing your own plugin ======================= Let's Encrypt client supports dynamic discovery of plugins through the `setuptools entry points`_. This way you can, for example, create a custom implementation of `~letsencrypt.interfaces.IAuthenticator` or the `~letsencrypt.interfaces.IInstaller` without having to merge it with the core upstream source code. An example is provided in ``examples/plugins/`` directory. .. warning:: Please be aware though that as this client is still in a developer-preview stage, the API may undergo a few changes. If you believe the plugin will be beneficial to the community, please consider submitting a pull request to the repo and we will update it with any necessary API changes. .. _`setuptools entry points`: https://pythonhosted.org/setuptools/setuptools.html#dynamic-discovery-of-services-and-plugins .. _coding-style: Coding style ============ Please: 1. **Be consistent with the rest of the code**. 2. Read `PEP 8 - Style Guide for Python Code`_. 3. Follow the `Google Python Style Guide`_, with the exception that we use `Sphinx-style`_ documentation:: def foo(arg): """Short description. :param int arg: Some number. :returns: Argument :rtype: int """ return arg 4. Remember to use ``pylint``. .. _Google Python Style Guide: https://google-styleguide.googlecode.com/svn/trunk/pyguide.html .. _Sphinx-style: http://sphinx-doc.org/ .. _PEP 8 - Style Guide for Python Code: https://www.python.org/dev/peps/pep-0008 Submitting a pull request ========================= Steps: 1. Write your code! 2. Make sure your environment is set up properly and that you're in your virtualenv. You can do this by running ``./tools/venv.sh``. (this is a **very important** step) 3. Run ``./pep8.travis.sh`` to do a cursory check of your code style. Fix any errors. 4. Run ``tox -e lint`` to check for pylint errors. Fix any errors. 5. Run ``tox`` to run the entire test suite including coverage. Fix any errors. 6. If your code touches communication with an ACME server/Boulder, you should run the integration tests, see `integration`_. See `Known Issues`_ for some common failures that have nothing to do with your code. 7. Submit the PR. 8. Did your tests pass on Travis? If they didn't, it might not be your fault! See `Known Issues`_. If it's not a known issue, fix any errors. .. _Known Issues: https://github.com/letsencrypt/letsencrypt/wiki/Known-issues Updating the documentation ========================== In order to generate the Sphinx documentation, run the following commands: .. code-block:: shell make -C docs clean html This should generate documentation in the ``docs/_build/html`` directory. Other methods for running the client ==================================== Vagrant ------- If you are a Vagrant user, Let's Encrypt comes with a Vagrantfile that automates setting up a development environment in an Ubuntu 14.04 LTS VM. To set it up, simply run ``vagrant up``. The repository is synced to ``/vagrant``, so you can get started with: .. code-block:: shell vagrant ssh cd /vagrant sudo ./venv/bin/letsencrypt Support for other Linux distributions coming soon. .. note:: Unfortunately, Python distutils and, by extension, setup.py and tox, use hard linking quite extensively. Hard linking is not supported by the default sync filesystem in Vagrant. As a result, all actions with these commands are *significantly slower* in Vagrant. One potential fix is to `use NFS`_ (`related issue`_). .. _use NFS: http://docs.vagrantup.com/v2/synced-folders/nfs.html .. _related issue: https://github.com/ClusterHQ/flocker/issues/516 Docker ------ OSX users will probably find it easiest to set up a Docker container for development. Let's Encrypt comes with a Dockerfile (``Dockerfile-dev``) for doing so. To use Docker on OSX, install and setup docker-machine using the instructions at https://docs.docker.com/installation/mac/. To build the development Docker image:: docker build -t letsencrypt -f Dockerfile-dev . Now run tests inside the Docker image: .. code-block:: shell docker run -it letsencrypt bash cd src tox -e py27 .. _prerequisites: Notes on OS dependencies ======================== OS-level dependencies can be installed like so: .. code-block:: shell letsencrypt-auto-source/letsencrypt-auto --os-packages-only In general... * ``sudo`` is required as a suggested way of running privileged process * `Python`_ 2.6/2.7 is required * `Augeas`_ is required for the Python bindings * ``virtualenv`` and ``pip`` are used for managing other python library dependencies .. _Python: https://wiki.python.org/moin/BeginnersGuide/Download .. _Augeas: http://augeas.net/ .. _Virtualenv: https://virtualenv.pypa.io Debian ------ For squeeze you will need to: - Use ``virtualenv --no-site-packages -p python`` instead of ``-p python2``. FreeBSD ------- Package installation for FreeBSD uses ``pkg``, not ports. FreeBSD by default uses ``tcsh``. In order to activate virtualenv (see below), you will need a compatible shell, e.g. ``pkg install bash && bash``. letsencrypt-0.4.1/docs/_static/0000755000175000017500000000000012665157717016063 5ustar bmwbmw00000000000000letsencrypt-0.4.1/docs/_static/.gitignore0000644000175000017500000000000012665157707020040 0ustar bmwbmw00000000000000letsencrypt-0.4.1/MANIFEST.in0000644000175000017500000000032112665157707015236 0ustar bmwbmw00000000000000include README.rst include CHANGES.rst include CONTRIBUTING.md include LICENSE.txt include linter_plugin.py recursive-include docs * recursive-include examples * recursive-include letsencrypt/tests/testdata * letsencrypt-0.4.1/letsencrypt/0000755000175000017500000000000012665157717016061 5ustar bmwbmw00000000000000letsencrypt-0.4.1/letsencrypt/crypto_util.py0000644000175000017500000002276612665157707021024 0ustar bmwbmw00000000000000"""Let's Encrypt client crypto utility functions. .. todo:: Make the transition to use PSS rather than PKCS1_v1_5 when the server is capable of handling the signatures. """ import logging import os import OpenSSL import pyrfc3339 import zope.component from acme import crypto_util as acme_crypto_util from acme import jose from letsencrypt import errors from letsencrypt import interfaces from letsencrypt import le_util logger = logging.getLogger(__name__) # High level functions def init_save_key(key_size, key_dir, keyname="key-letsencrypt.pem"): """Initializes and saves a privkey. Inits key and saves it in PEM format on the filesystem. .. note:: keyname is the attempted filename, it may be different if a file already exists at the path. :param int key_size: RSA key size in bits :param str key_dir: Key save directory. :param str keyname: Filename of key :returns: Key :rtype: :class:`letsencrypt.le_util.Key` :raises ValueError: If unable to generate the key given key_size. """ try: key_pem = make_key(key_size) except ValueError as err: logger.exception(err) raise err config = zope.component.getUtility(interfaces.IConfig) # Save file le_util.make_or_verify_dir(key_dir, 0o700, os.geteuid(), config.strict_permissions) key_f, key_path = le_util.unique_file( os.path.join(key_dir, keyname), 0o600) with key_f: key_f.write(key_pem) logger.info("Generating key (%d bits): %s", key_size, key_path) return le_util.Key(key_path, key_pem) def init_save_csr(privkey, names, path, csrname="csr-letsencrypt.pem"): """Initialize a CSR with the given private key. :param privkey: Key to include in the CSR :type privkey: :class:`letsencrypt.le_util.Key` :param set names: `str` names to include in the CSR :param str path: Certificate save directory. :returns: CSR :rtype: :class:`letsencrypt.le_util.CSR` """ csr_pem, csr_der = make_csr(privkey.pem, names) config = zope.component.getUtility(interfaces.IConfig) # Save CSR le_util.make_or_verify_dir(path, 0o755, os.geteuid(), config.strict_permissions) csr_f, csr_filename = le_util.unique_file( os.path.join(path, csrname), 0o644) csr_f.write(csr_pem) csr_f.close() logger.info("Creating CSR: %s", csr_filename) return le_util.CSR(csr_filename, csr_der, "der") # Lower level functions def make_csr(key_str, domains): """Generate a CSR. :param str key_str: PEM-encoded RSA key. :param list domains: Domains included in the certificate. .. todo:: Detect duplicates in `domains`? Using a set doesn't preserve order... :returns: new CSR in PEM and DER form containing all domains :rtype: tuple """ assert domains, "Must provide one or more hostnames for the CSR." pkey = OpenSSL.crypto.load_privatekey(OpenSSL.crypto.FILETYPE_PEM, key_str) req = OpenSSL.crypto.X509Req() req.get_subject().CN = domains[0] # TODO: what to put into req.get_subject()? # TODO: put SAN if len(domains) > 1 req.add_extensions([ OpenSSL.crypto.X509Extension( "subjectAltName", critical=False, value=", ".join("DNS:%s" % d for d in domains) ), ]) req.set_version(2) req.set_pubkey(pkey) req.sign(pkey, "sha256") return tuple(OpenSSL.crypto.dump_certificate_request(method, req) for method in (OpenSSL.crypto.FILETYPE_PEM, OpenSSL.crypto.FILETYPE_ASN1)) # WARNING: the csr and private key file are possible attack vectors for TOCTOU # We should either... # A. Do more checks to verify that the CSR is trusted/valid # B. Audit the parsing code for vulnerabilities def valid_csr(csr): """Validate CSR. Check if `csr` is a valid CSR for the given domains. :param str csr: CSR in PEM. :returns: Validity of CSR. :rtype: bool """ try: req = OpenSSL.crypto.load_certificate_request( OpenSSL.crypto.FILETYPE_PEM, csr) return req.verify(req.get_pubkey()) except OpenSSL.crypto.Error as error: logger.debug(error, exc_info=True) return False def csr_matches_pubkey(csr, privkey): """Does private key correspond to the subject public key in the CSR? :param str csr: CSR in PEM. :param str privkey: Private key file contents (PEM) :returns: Correspondence of private key to CSR subject public key. :rtype: bool """ req = OpenSSL.crypto.load_certificate_request( OpenSSL.crypto.FILETYPE_PEM, csr) pkey = OpenSSL.crypto.load_privatekey(OpenSSL.crypto.FILETYPE_PEM, privkey) try: return req.verify(pkey) except OpenSSL.crypto.Error as error: logger.debug(error, exc_info=True) return False def make_key(bits): """Generate PEM encoded RSA key. :param int bits: Number of bits, at least 1024. :returns: new RSA key in PEM form with specified number of bits :rtype: str """ assert bits >= 1024 # XXX key = OpenSSL.crypto.PKey() key.generate_key(OpenSSL.crypto.TYPE_RSA, bits) return OpenSSL.crypto.dump_privatekey(OpenSSL.crypto.FILETYPE_PEM, key) def valid_privkey(privkey): """Is valid RSA private key? :param str privkey: Private key file contents in PEM :returns: Validity of private key. :rtype: bool """ try: return OpenSSL.crypto.load_privatekey( OpenSSL.crypto.FILETYPE_PEM, privkey).check() except (TypeError, OpenSSL.crypto.Error): return False def pyopenssl_load_certificate(data): """Load PEM/DER certificate. :raises errors.Error: """ openssl_errors = [] for file_type in (OpenSSL.crypto.FILETYPE_PEM, OpenSSL.crypto.FILETYPE_ASN1): try: return OpenSSL.crypto.load_certificate(file_type, data), file_type except OpenSSL.crypto.Error as error: # TODO: other errors? openssl_errors.append(error) raise errors.Error("Unable to load: {0}".format(",".join( str(error) for error in openssl_errors))) def _get_sans_from_cert_or_req(cert_or_req_str, load_func, typ=OpenSSL.crypto.FILETYPE_PEM): try: cert_or_req = load_func(typ, cert_or_req_str) except OpenSSL.crypto.Error as error: logger.exception(error) raise # pylint: disable=protected-access return acme_crypto_util._pyopenssl_cert_or_req_san(cert_or_req) def get_sans_from_cert(cert, typ=OpenSSL.crypto.FILETYPE_PEM): """Get a list of Subject Alternative Names from a certificate. :param str cert: Certificate (encoded). :param typ: `OpenSSL.crypto.FILETYPE_PEM` or `OpenSSL.crypto.FILETYPE_ASN1` :returns: A list of Subject Alternative Names. :rtype: list """ return _get_sans_from_cert_or_req( cert, OpenSSL.crypto.load_certificate, typ) def get_sans_from_csr(csr, typ=OpenSSL.crypto.FILETYPE_PEM): """Get a list of Subject Alternative Names from a CSR. :param str csr: CSR (encoded). :param typ: `OpenSSL.crypto.FILETYPE_PEM` or `OpenSSL.crypto.FILETYPE_ASN1` :returns: A list of Subject Alternative Names. :rtype: list """ return _get_sans_from_cert_or_req( csr, OpenSSL.crypto.load_certificate_request, typ) def dump_pyopenssl_chain(chain, filetype=OpenSSL.crypto.FILETYPE_PEM): """Dump certificate chain into a bundle. :param list chain: List of `OpenSSL.crypto.X509` (or wrapped in `acme.jose.ComparableX509`). """ # XXX: returns empty string when no chain is available, which # shuts up RenewableCert, but might not be the best solution... def _dump_cert(cert): if isinstance(cert, jose.ComparableX509): # pylint: disable=protected-access cert = cert.wrapped return OpenSSL.crypto.dump_certificate(filetype, cert) # assumes that OpenSSL.crypto.dump_certificate includes ending # newline character return "".join(_dump_cert(cert) for cert in chain) def notBefore(cert_path): """When does the cert at cert_path start being valid? :param str cert_path: path to a cert in PEM format :returns: the notBefore value from the cert at cert_path :rtype: :class:`datetime.datetime` """ return _notAfterBefore(cert_path, OpenSSL.crypto.X509.get_notBefore) def notAfter(cert_path): """When does the cert at cert_path stop being valid? :param str cert_path: path to a cert in PEM format :returns: the notAfter value from the cert at cert_path :rtype: :class:`datetime.datetime` """ return _notAfterBefore(cert_path, OpenSSL.crypto.X509.get_notAfter) def _notAfterBefore(cert_path, method): """Internal helper function for finding notbefore/notafter. :param str cert_path: path to a cert in PEM format :param function method: one of ``OpenSSL.crypto.X509.get_notBefore`` or ``OpenSSL.crypto.X509.get_notAfter`` :returns: the notBefore or notAfter value from the cert at cert_path :rtype: :class:`datetime.datetime` """ with open(cert_path) as f: x509 = OpenSSL.crypto.load_certificate(OpenSSL.crypto.FILETYPE_PEM, f.read()) timestamp = method(x509) reformatted_timestamp = [timestamp[0:4], "-", timestamp[4:6], "-", timestamp[6:8], "T", timestamp[8:10], ":", timestamp[10:12], ":", timestamp[12:]] return pyrfc3339.parse("".join(reformatted_timestamp)) letsencrypt-0.4.1/letsencrypt/client.py0000644000175000017500000005316512665157707017722 0ustar bmwbmw00000000000000"""Let's Encrypt client API.""" import logging import os from cryptography.hazmat.backends import default_backend from cryptography.hazmat.primitives.asymmetric import rsa import OpenSSL import zope.component from acme import client as acme_client from acme import jose from acme import messages import letsencrypt from letsencrypt import account from letsencrypt import auth_handler from letsencrypt import configuration from letsencrypt import constants from letsencrypt import continuity_auth from letsencrypt import crypto_util from letsencrypt import errors from letsencrypt import error_handler from letsencrypt import interfaces from letsencrypt import le_util from letsencrypt import reverter from letsencrypt import storage from letsencrypt.display import ops as display_ops from letsencrypt.display import enhancements logger = logging.getLogger(__name__) def acme_from_config_key(config, key): "Wrangle ACME client construction" # TODO: Allow for other alg types besides RS256 net = acme_client.ClientNetwork(key, verify_ssl=(not config.no_verify_ssl), user_agent=_determine_user_agent(config)) return acme_client.Client(config.server, key=key, net=net) def _determine_user_agent(config): """ Set a user_agent string in the config based on the choice of plugins. (this wasn't knowable at construction time) :returns: the client's User-Agent string :rtype: `str` """ if config.user_agent is None: ua = "LetsEncryptPythonClient/{0} ({1}) Authenticator/{2} Installer/{3}" ua = ua.format(letsencrypt.__version__, " ".join(le_util.get_os_info()), config.authenticator, config.installer) else: ua = config.user_agent return ua def register(config, account_storage, tos_cb=None): """Register new account with an ACME CA. This function takes care of generating fresh private key, registering the account, optionally accepting CA Terms of Service and finally saving the account. It should be called prior to initialization of `Client`, unless account has already been created. :param .IConfig config: Client configuration. :param .AccountStorage account_storage: Account storage where newly registered account will be saved to. Save happens only after TOS acceptance step, so any account private keys or `.RegistrationResource` will not be persisted if `tos_cb` returns ``False``. :param tos_cb: If ACME CA requires the user to accept a Terms of Service before registering account, client action is necessary. For example, a CLI tool would prompt the user acceptance. `tos_cb` must be a callable that should accept `.RegistrationResource` and return a `bool`: ``True`` iff the Terms of Service present in the contained `.Registration.terms_of_service` is accepted by the client, and ``False`` otherwise. ``tos_cb`` will be called only if the client acction is necessary, i.e. when ``terms_of_service is not None``. This argument is optional, if not supplied it will default to automatic acceptance! :raises letsencrypt.errors.Error: In case of any client problems, in particular registration failure, or unaccepted Terms of Service. :raises acme.errors.Error: In case of any protocol problems. :returns: Newly registered and saved account, as well as protocol API handle (should be used in `Client` initialization). :rtype: `tuple` of `.Account` and `acme.client.Client` """ # Log non-standard actions, potentially wrong API calls if account_storage.find_all(): logger.info("There are already existing accounts for %s", config.server) if config.email is None: if not config.register_unsafely_without_email: msg = ("No email was provided and " "--register-unsafely-without-email was not present.") logger.warn(msg) raise errors.Error(msg) logger.warn("Registering without email!") # Each new registration shall use a fresh new key key = jose.JWKRSA(key=jose.ComparableRSAKey( rsa.generate_private_key( public_exponent=65537, key_size=config.rsa_key_size, backend=default_backend()))) acme = acme_from_config_key(config, key) # TODO: add phone? regr = perform_registration(acme, config) if regr.terms_of_service is not None: if tos_cb is not None and not tos_cb(regr): raise errors.Error( "Registration cannot proceed without accepting " "Terms of Service.") regr = acme.agree_to_tos(regr) acc = account.Account(regr, key) account.report_new_account(acc, config) account_storage.save(acc) return acc, acme def perform_registration(acme, config): """ Actually register new account, trying repeatedly if there are email problems :param .IConfig config: Client configuration. :param acme.client.Client client: ACME client object. :returns: Registration Resource. :rtype: `acme.messages.RegistrationResource` :raises .UnexpectedUpdate: """ try: return acme.register(messages.NewRegistration.from_data(email=config.email)) except messages.Error as e: err = repr(e) if "MX record" in err or "Validation of contact mailto" in err: config.namespace.email = display_ops.get_email(more=True, invalid=True) return perform_registration(acme, config) else: raise class Client(object): """ACME protocol client. :ivar .IConfig config: Client configuration. :ivar .Account account: Account registered with `register`. :ivar .AuthHandler auth_handler: Authorizations handler that will dispatch DV and Continuity challenges to appropriate authenticators (providing `.IAuthenticator` interface). :ivar .IAuthenticator dv_auth: Prepared (`.IAuthenticator.prepare`) authenticator that can solve the `.constants.DV_CHALLENGES`. :ivar .IInstaller installer: Installer. :ivar acme.client.Client acme: Optional ACME client API handle. You might already have one from `register`. """ def __init__(self, config, account_, dv_auth, installer, acme=None): """Initialize a client.""" self.config = config self.account = account_ self.dv_auth = dv_auth self.installer = installer # Initialize ACME if account is provided if acme is None and self.account is not None: acme = acme_from_config_key(config, self.account.key) self.acme = acme # TODO: Check if self.config.enroll_autorenew is None. If # so, set it based to the default: figure out if dv_auth is # standalone (then default is False, otherwise default is True) if dv_auth is not None: cont_auth = continuity_auth.ContinuityAuthenticator(config, installer) self.auth_handler = auth_handler.AuthHandler( dv_auth, cont_auth, self.acme, self.account) else: self.auth_handler = None def obtain_certificate_from_csr(self, domains, csr, typ=OpenSSL.crypto.FILETYPE_ASN1): """Obtain certificate. Internal function with precondition that `domains` are consistent with identifiers present in the `csr`. :param list domains: Domain names. :param .le_util.CSR csr: DER-encoded Certificate Signing Request. The key used to generate this CSR can be different than `authkey`. :returns: `.CertificateResource` and certificate chain (as returned by `.fetch_chain`). :rtype: tuple """ if self.auth_handler is None: msg = ("Unable to obtain certificate because authenticator is " "not set.") logger.warning(msg) raise errors.Error(msg) if self.account.regr is None: raise errors.Error("Please register with the ACME server first.") logger.debug("CSR: %s, domains: %s", csr, domains) authzr = self.auth_handler.get_authorizations(domains) certr = self.acme.request_issuance( jose.ComparableX509( OpenSSL.crypto.load_certificate_request(typ, csr.data)), authzr) return certr, self.acme.fetch_chain(certr) def obtain_certificate(self, domains): """Obtains a certificate from the ACME server. `.register` must be called before `.obtain_certificate` :param list domains: domains to get a certificate :returns: `.CertificateResource`, certificate chain (as returned by `.fetch_chain`), and newly generated private key (`.le_util.Key`) and DER-encoded Certificate Signing Request (`.le_util.CSR`). :rtype: tuple """ # Create CSR from names key = crypto_util.init_save_key( self.config.rsa_key_size, self.config.key_dir) csr = crypto_util.init_save_csr(key, domains, self.config.csr_dir) return self.obtain_certificate_from_csr(domains, csr) + (key, csr) def obtain_and_enroll_certificate(self, domains): """Obtain and enroll certificate. Get a new certificate for the specified domains using the specified authenticator and installer, and then create a new renewable lineage containing it. :param list domains: Domains to request. :param plugins: A PluginsFactory object. :returns: A new :class:`letsencrypt.storage.RenewableCert` instance referred to the enrolled cert lineage, False if the cert could not be obtained, or None if doing a successful dry run. """ certr, chain, key, _ = self.obtain_certificate(domains) if (self.config.config_dir != constants.CLI_DEFAULTS["config_dir"] or self.config.work_dir != constants.CLI_DEFAULTS["work_dir"]): logger.warning( "Non-standard path(s), might not work with crontab installed " "by your operating system package manager") if self.config.dry_run: logger.info("Dry run: Skipping creating new lineage for %s", domains[0]) return None else: return storage.RenewableCert.new_lineage( domains[0], OpenSSL.crypto.dump_certificate( OpenSSL.crypto.FILETYPE_PEM, certr.body.wrapped), key.pem, crypto_util.dump_pyopenssl_chain(chain), configuration.RenewerConfiguration(self.config.namespace)) def save_certificate(self, certr, chain_cert, cert_path, chain_path, fullchain_path): """Saves the certificate received from the ACME server. :param certr: ACME "certificate" resource. :type certr: :class:`acme.messages.Certificate` :param list chain_cert: :param str cert_path: Candidate path to a certificate. :param str chain_path: Candidate path to a certificate chain. :param str fullchain_path: Candidate path to a full cert chain. :returns: cert_path, chain_path, and fullchain_path as absolute paths to the actual files :rtype: `tuple` of `str` :raises IOError: If unable to find room to write the cert files """ for path in cert_path, chain_path, fullchain_path: le_util.make_or_verify_dir( os.path.dirname(path), 0o755, os.geteuid(), self.config.strict_permissions) cert_pem = OpenSSL.crypto.dump_certificate( OpenSSL.crypto.FILETYPE_PEM, certr.body.wrapped) cert_file, act_cert_path = le_util.unique_file(cert_path, 0o644) try: cert_file.write(cert_pem) finally: cert_file.close() logger.info("Server issued certificate; certificate written to %s", act_cert_path) cert_chain_abspath = None fullchain_abspath = None if chain_cert: chain_pem = crypto_util.dump_pyopenssl_chain(chain_cert) cert_chain_abspath = _save_chain(chain_pem, chain_path) fullchain_abspath = _save_chain(cert_pem + chain_pem, fullchain_path) return os.path.abspath(act_cert_path), cert_chain_abspath, fullchain_abspath def deploy_certificate(self, domains, privkey_path, cert_path, chain_path, fullchain_path): """Install certificate :param list domains: list of domains to install the certificate :param str privkey_path: path to certificate private key :param str cert_path: certificate file path (optional) :param str chain_path: chain file path """ if self.installer is None: logger.warning("No installer specified, client is unable to deploy" "the certificate") raise errors.Error("No installer available") chain_path = None if chain_path is None else os.path.abspath(chain_path) with error_handler.ErrorHandler(self.installer.recovery_routine): for dom in domains: self.installer.deploy_cert( domain=dom, cert_path=os.path.abspath(cert_path), key_path=os.path.abspath(privkey_path), chain_path=chain_path, fullchain_path=fullchain_path) self.installer.save() # needed by the Apache plugin self.installer.save("Deployed Let's Encrypt Certificate") msg = ("We were unable to install your certificate, " "however, we successfully restored your " "server to its prior configuration.") with error_handler.ErrorHandler(self._rollback_and_restart, msg): # sites may have been enabled / final cleanup self.installer.restart() def enhance_config(self, domains, config): """Enhance the configuration. :param list domains: list of domains to configure :ivar config: Namespace typically produced by :meth:`argparse.ArgumentParser.parse_args`. it must have the redirect, hsts and uir attributes. :type namespace: :class:`argparse.Namespace` :raises .errors.Error: if no installer is specified in the client. """ if self.installer is None: logger.warning("No installer is specified, there isn't any " "configuration to enhance.") raise errors.Error("No installer available") if config is None: logger.warning("No config is specified.") raise errors.Error("No config available") supported = self.installer.supported_enhancements() redirect = config.redirect if "redirect" in supported else False hsts = config.hsts if "ensure-http-header" in supported else False uir = config.uir if "ensure-http-header" in supported else False if redirect is None: redirect = enhancements.ask("redirect") if redirect: self.apply_enhancement(domains, "redirect") if hsts: self.apply_enhancement(domains, "ensure-http-header", "Strict-Transport-Security") if uir: self.apply_enhancement(domains, "ensure-http-header", "Upgrade-Insecure-Requests") msg = ("We were unable to restart web server") if redirect or hsts or uir: with error_handler.ErrorHandler(self._rollback_and_restart, msg): self.installer.restart() def apply_enhancement(self, domains, enhancement, options=None): """Applies an enhacement on all domains. :param domains: list of ssl_vhosts :type list of str :param enhancement: name of enhancement, e.g. ensure-http-header :type str .. note:: when more options are need make options a list. :param options: options to enhancement, e.g. Strict-Transport-Security :type str :raises .errors.PluginError: If Enhancement is not supported, or if there is any other problem with the enhancement. """ msg = ("We were unable to set up enhancement %s for your server, " "however, we successfully installed your certificate." % (enhancement)) with error_handler.ErrorHandler(self._recovery_routine_with_msg, msg): for dom in domains: try: self.installer.enhance(dom, enhancement, options) except errors.PluginEnhancementAlreadyPresent: logger.warn("Enhancement %s was already set.", enhancement) except errors.PluginError: logger.warn("Unable to set enhancement %s for %s", enhancement, dom) raise self.installer.save("Add enhancement %s" % (enhancement)) def _recovery_routine_with_msg(self, success_msg): """Calls the installer's recovery routine and prints success_msg :param str success_msg: message to show on successful recovery """ self.installer.recovery_routine() reporter = zope.component.getUtility(interfaces.IReporter) reporter.add_message(success_msg, reporter.HIGH_PRIORITY) def _rollback_and_restart(self, success_msg): """Rollback the most recent checkpoint and restart the webserver :param str success_msg: message to show on successful rollback """ logger.critical("Rolling back to previous server configuration...") reporter = zope.component.getUtility(interfaces.IReporter) try: self.installer.rollback_checkpoints() self.installer.restart() except: # TODO: suggest letshelp-letsencypt here reporter.add_message( "An error occurred and we failed to restore your config and " "restart your server. Please submit a bug report to " "https://github.com/letsencrypt/letsencrypt", reporter.HIGH_PRIORITY) raise reporter.add_message(success_msg, reporter.HIGH_PRIORITY) def validate_key_csr(privkey, csr=None): """Validate Key and CSR files. Verifies that the client key and csr arguments are valid and correspond to one another. This does not currently check the names in the CSR due to the inability to read SANs from CSRs in python crypto libraries. If csr is left as None, only the key will be validated. :param privkey: Key associated with CSR :type privkey: :class:`letsencrypt.le_util.Key` :param .le_util.CSR csr: CSR :raises .errors.Error: when validation fails """ # TODO: Handle all of these problems appropriately # The client can eventually do things like prompt the user # and allow the user to take more appropriate actions # Key must be readable and valid. if privkey.pem and not crypto_util.valid_privkey(privkey.pem): raise errors.Error("The provided key is not a valid key") if csr: if csr.form == "der": csr_obj = OpenSSL.crypto.load_certificate_request( OpenSSL.crypto.FILETYPE_ASN1, csr.data) csr = le_util.CSR(csr.file, OpenSSL.crypto.dump_certificate( OpenSSL.crypto.FILETYPE_PEM, csr_obj), "pem") # If CSR is provided, it must be readable and valid. if csr.data and not crypto_util.valid_csr(csr.data): raise errors.Error("The provided CSR is not a valid CSR") # If both CSR and key are provided, the key must be the same key used # in the CSR. if csr.data and privkey.pem: if not crypto_util.csr_matches_pubkey( csr.data, privkey.pem): raise errors.Error("The key and CSR do not match") def rollback(default_installer, checkpoints, config, plugins): """Revert configuration the specified number of checkpoints. :param int checkpoints: Number of checkpoints to revert. :param config: Configuration. :type config: :class:`letsencrypt.interfaces.IConfig` """ # Misconfigurations are only a slight problems... allow the user to rollback installer = display_ops.pick_installer( config, default_installer, plugins, question="Which installer " "should be used for rollback?") # No Errors occurred during init... proceed normally # If installer is None... couldn't find an installer... there shouldn't be # anything to rollback if installer is not None: installer.rollback_checkpoints(checkpoints) installer.restart() def view_config_changes(config): """View checkpoints and associated configuration changes. .. note:: This assumes that the installation is using a Reverter object. :param config: Configuration. :type config: :class:`letsencrypt.interfaces.IConfig` """ rev = reverter.Reverter(config) rev.recovery_routine() rev.view_config_changes() def _save_chain(chain_pem, chain_path): """Saves chain_pem at a unique path based on chain_path. :param str chain_pem: certificate chain in PEM format :param str chain_path: candidate path for the cert chain :returns: absolute path to saved cert chain :rtype: str """ chain_file, act_chain_path = le_util.unique_file(chain_path, 0o644) try: chain_file.write(chain_pem) finally: chain_file.close() logger.info("Cert chain written to %s", act_chain_path) # This expects a valid chain file return os.path.abspath(act_chain_path) letsencrypt-0.4.1/letsencrypt/colored_logging.py0000644000175000017500000000275012665157707021573 0ustar bmwbmw00000000000000"""A formatter and StreamHandler for colorizing logging output.""" import logging import sys from letsencrypt import le_util class StreamHandler(logging.StreamHandler): """Sends colored logging output to a stream. If the specified stream is not a tty, the class works like the standard logging.StreamHandler. Default red_level is logging.WARNING. :ivar bool colored: True if output should be colored :ivar bool red_level: The level at which to output """ def __init__(self, stream=None): if sys.version_info < (2, 7): # pragma: no cover # pylint: disable=non-parent-init-called logging.StreamHandler.__init__(self, stream) else: super(StreamHandler, self).__init__(stream) self.colored = (sys.stderr.isatty() if stream is None else stream.isatty()) self.red_level = logging.WARNING def format(self, record): """Formats the string representation of record. :param logging.LogRecord record: Record to be formatted :returns: Formatted, string representation of record :rtype: str """ out = (logging.StreamHandler.format(self, record) if sys.version_info < (2, 7) else super(StreamHandler, self).format(record)) if self.colored and record.levelno >= self.red_level: return ''.join((le_util.ANSI_SGR_RED, out, le_util.ANSI_SGR_RESET)) else: return out letsencrypt-0.4.1/letsencrypt/configuration.py0000644000175000017500000001107312665157707021303 0ustar bmwbmw00000000000000"""Let's Encrypt user-supplied configuration.""" import copy import os from six.moves.urllib import parse # pylint: disable=import-error import zope.interface from letsencrypt import constants from letsencrypt import errors from letsencrypt import interfaces from letsencrypt import le_util @zope.interface.implementer(interfaces.IConfig) class NamespaceConfig(object): """Configuration wrapper around :class:`argparse.Namespace`. For more documentation, including available attributes, please see :class:`letsencrypt.interfaces.IConfig`. However, note that the following attributes are dynamically resolved using :attr:`~letsencrypt.interfaces.IConfig.work_dir` and relative paths defined in :py:mod:`letsencrypt.constants`: - `accounts_dir` - `csr_dir` - `in_progress_dir` - `key_dir` - `renewer_config_file` - `temp_checkpoint_dir` :ivar namespace: Namespace typically produced by :meth:`argparse.ArgumentParser.parse_args`. :type namespace: :class:`argparse.Namespace` """ def __init__(self, namespace): self.namespace = namespace self.namespace.config_dir = os.path.abspath(self.namespace.config_dir) self.namespace.work_dir = os.path.abspath(self.namespace.work_dir) self.namespace.logs_dir = os.path.abspath(self.namespace.logs_dir) # Check command line parameters sanity, and error out in case of problem. check_config_sanity(self) def __getattr__(self, name): return getattr(self.namespace, name) @property def server_path(self): """File path based on ``server``.""" parsed = parse.urlparse(self.namespace.server) return (parsed.netloc + parsed.path).replace('/', os.path.sep) @property def accounts_dir(self): # pylint: disable=missing-docstring return os.path.join( self.namespace.config_dir, constants.ACCOUNTS_DIR, self.server_path) @property def backup_dir(self): # pylint: disable=missing-docstring return os.path.join(self.namespace.work_dir, constants.BACKUP_DIR) @property def csr_dir(self): # pylint: disable=missing-docstring return os.path.join(self.namespace.config_dir, constants.CSR_DIR) @property def in_progress_dir(self): # pylint: disable=missing-docstring return os.path.join(self.namespace.work_dir, constants.IN_PROGRESS_DIR) @property def key_dir(self): # pylint: disable=missing-docstring return os.path.join(self.namespace.config_dir, constants.KEY_DIR) @property def temp_checkpoint_dir(self): # pylint: disable=missing-docstring return os.path.join( self.namespace.work_dir, constants.TEMP_CHECKPOINT_DIR) def __deepcopy__(self, _memo): # Work around https://bugs.python.org/issue1515 for py26 tests :( :( # https://travis-ci.org/letsencrypt/letsencrypt/jobs/106900743#L3276 new_ns = copy.deepcopy(self.namespace) return type(self)(new_ns) class RenewerConfiguration(object): """Configuration wrapper for renewer.""" def __init__(self, namespace): self.namespace = namespace def __getattr__(self, name): return getattr(self.namespace, name) @property def archive_dir(self): # pylint: disable=missing-docstring return os.path.join(self.namespace.config_dir, constants.ARCHIVE_DIR) @property def live_dir(self): # pylint: disable=missing-docstring return os.path.join(self.namespace.config_dir, constants.LIVE_DIR) @property def renewal_configs_dir(self): # pylint: disable=missing-docstring return os.path.join( self.namespace.config_dir, constants.RENEWAL_CONFIGS_DIR) @property def renewer_config_file(self): # pylint: disable=missing-docstring return os.path.join( self.namespace.config_dir, constants.RENEWER_CONFIG_FILENAME) def check_config_sanity(config): """Validate command line options and display error message if requirements are not met. :param config: IConfig instance holding user configuration :type args: :class:`letsencrypt.interfaces.IConfig` """ # Port check if config.http01_port == config.tls_sni_01_port: raise errors.ConfigurationError( "Trying to run http-01 and tls-sni-01 " "on the same port ({0})".format(config.tls_sni_01_port)) # Domain checks if config.namespace.domains is not None: for domain in config.namespace.domains: # This may be redundant, but let's be paranoid le_util.enforce_domain_sanity(domain) letsencrypt-0.4.1/letsencrypt/__init__.py0000644000175000017500000000016712665157707020175 0ustar bmwbmw00000000000000"""Let's Encrypt client.""" # version number like 1.2.3a0, must have at least 2 parts, like 1.2 __version__ = '0.4.1' letsencrypt-0.4.1/letsencrypt/achallenges.py0000644000175000017500000000372512665157707020707 0ustar bmwbmw00000000000000"""Client annotated ACME challenges. Please use names such as ``achall`` to distiguish from variables "of type" :class:`acme.challenges.Challenge` (denoted by ``chall``) and :class:`.ChallengeBody` (denoted by ``challb``):: from acme import challenges from acme import messages from letsencrypt import achallenges chall = challenges.DNS(token='foo') challb = messages.ChallengeBody(chall=chall) achall = achallenges.DNS(chall=challb, domain='example.com') Note, that all annotated challenges act as a proxy objects:: achall.token == challb.token """ import logging from acme import challenges from acme import jose logger = logging.getLogger(__name__) # pylint: disable=too-few-public-methods class AnnotatedChallenge(jose.ImmutableMap): """Client annotated challenge. Wraps around server provided challenge and annotates with data useful for the client. :ivar challb: Wrapped `~.ChallengeBody`. """ __slots__ = ('challb',) acme_type = NotImplemented def __getattr__(self, name): return getattr(self.challb, name) class KeyAuthorizationAnnotatedChallenge(AnnotatedChallenge): """Client annotated `KeyAuthorizationChallenge` challenge.""" __slots__ = ('challb', 'domain', 'account_key') def response_and_validation(self, *args, **kwargs): """Generate response and validation.""" return self.challb.chall.response_and_validation( self.account_key, *args, **kwargs) class DNS(AnnotatedChallenge): """Client annotated "dns" ACME challenge.""" __slots__ = ('challb', 'domain') acme_type = challenges.DNS class RecoveryContact(AnnotatedChallenge): """Client annotated "recoveryContact" ACME challenge.""" __slots__ = ('challb', 'domain') acme_type = challenges.RecoveryContact class ProofOfPossession(AnnotatedChallenge): """Client annotated "proofOfPossession" ACME challenge.""" __slots__ = ('challb', 'domain') acme_type = challenges.ProofOfPossession letsencrypt-0.4.1/letsencrypt/interfaces.py0000644000175000017500000004040512665157707020560 0ustar bmwbmw00000000000000"""Let's Encrypt client interfaces.""" import abc import zope.interface # pylint: disable=no-self-argument,no-method-argument,no-init,inherit-non-class # pylint: disable=too-few-public-methods class AccountStorage(object): """Accounts storage interface.""" __metaclass__ = abc.ABCMeta @abc.abstractmethod def find_all(self): # pragma: no cover """Find all accounts. :returns: All found accounts. :rtype: list """ raise NotImplementedError() @abc.abstractmethod def load(self, account_id): # pragma: no cover """Load an account by its id. :raises .AccountNotFound: if account could not be found :raises .AccountStorageError: if account could not be loaded """ raise NotImplementedError() @abc.abstractmethod def save(self, account): # pragma: no cover """Save account. :raises .AccountStorageError: if account could not be saved """ raise NotImplementedError() class IPluginFactory(zope.interface.Interface): """IPlugin factory. Objects providing this interface will be called without satisfying any entry point "extras" (extra dependencies) you might have defined for your plugin, e.g (excerpt from ``setup.py`` script):: setup( ... entry_points={ 'letsencrypt.plugins': [ 'name=example_project.plugin[plugin_deps]', ], }, extras_require={ 'plugin_deps': ['dep1', 'dep2'], } ) Therefore, make sure such objects are importable and usable without extras. This is necessary, because CLI does the following operations (in order): - loads an entry point, - calls `inject_parser_options`, - requires an entry point, - creates plugin instance (`__call__`). """ description = zope.interface.Attribute("Short plugin description") def __call__(config, name): """Create new `IPlugin`. :param IConfig config: Configuration. :param str name: Unique plugin name. """ def inject_parser_options(parser, name): """Inject argument parser options (flags). 1. Be nice and prepend all options and destinations with `~.common.option_namespace` and `~common.dest_namespace`. 2. Inject options (flags) only. Positional arguments are not allowed, as this would break the CLI. :param ArgumentParser parser: (Almost) top-level CLI parser. :param str name: Unique plugin name. """ class IPlugin(zope.interface.Interface): """Let's Encrypt plugin.""" def prepare(): """Prepare the plugin. Finish up any additional initialization. :raises .PluginError: when full initialization cannot be completed. :raises .MisconfigurationError: when full initialization cannot be completed. Plugin will be displayed on a list of available plugins. :raises .NoInstallationError: when the necessary programs/files cannot be located. Plugin will NOT be displayed on a list of available plugins. :raises .NotSupportedError: when the installation is recognized, but the version is not currently supported. """ def more_info(): """Human-readable string to help the user. Should describe the steps taken and any relevant info to help the user decide which plugin to use. :rtype str: """ class IAuthenticator(IPlugin): """Generic Let's Encrypt Authenticator. Class represents all possible tools processes that have the ability to perform challenges and attain a certificate. """ def get_chall_pref(domain): """Return list of challenge preferences. :param str domain: Domain for which challenge preferences are sought. :returns: List of challenge types (subclasses of :class:`acme.challenges.Challenge`) with the most preferred challenges first. If a type is not specified, it means the Authenticator cannot perform the challenge. :rtype: list """ def perform(achalls): """Perform the given challenge. :param list achalls: Non-empty (guaranteed) list of :class:`~letsencrypt.achallenges.AnnotatedChallenge` instances, such that it contains types found within :func:`get_chall_pref` only. :returns: List of ACME :class:`~acme.challenges.ChallengeResponse` instances or if the :class:`~acme.challenges.Challenge` cannot be fulfilled then: ``None`` Authenticator can perform challenge, but not at this time. ``False`` Authenticator will never be able to perform (error). :rtype: :class:`list` of :class:`acme.challenges.ChallengeResponse`, where responses are required to be returned in the same order as corresponding input challenges :raises .PluginError: If challenges cannot be performed """ def cleanup(achalls): """Revert changes and shutdown after challenges complete. :param list achalls: Non-empty (guaranteed) list of :class:`~letsencrypt.achallenges.AnnotatedChallenge` instances, a subset of those previously passed to :func:`perform`. :raises PluginError: if original configuration cannot be restored """ class IConfig(zope.interface.Interface): """Let's Encrypt user-supplied configuration. .. warning:: The values stored in the configuration have not been filtered, stripped or sanitized. """ server = zope.interface.Attribute("ACME Directory Resource URI.") email = zope.interface.Attribute( "Email used for registration and recovery contact.") rsa_key_size = zope.interface.Attribute("Size of the RSA key.") config_dir = zope.interface.Attribute("Configuration directory.") work_dir = zope.interface.Attribute("Working directory.") accounts_dir = zope.interface.Attribute( "Directory where all account information is stored.") backup_dir = zope.interface.Attribute("Configuration backups directory.") csr_dir = zope.interface.Attribute( "Directory where newly generated Certificate Signing Requests " "(CSRs) are saved.") in_progress_dir = zope.interface.Attribute( "Directory used before a permanent checkpoint is finalized.") key_dir = zope.interface.Attribute("Keys storage.") temp_checkpoint_dir = zope.interface.Attribute( "Temporary checkpoint directory.") renewer_config_file = zope.interface.Attribute( "Location of renewal configuration file.") no_verify_ssl = zope.interface.Attribute( "Disable SSL certificate verification.") tls_sni_01_port = zope.interface.Attribute( "Port number to perform tls-sni-01 challenge. " "Boulder in testing mode defaults to 5001.") http01_port = zope.interface.Attribute( "Port used in the SimpleHttp challenge.") class IInstaller(IPlugin): """Generic Let's Encrypt Installer Interface. Represents any server that an X509 certificate can be placed. """ def get_all_names(): """Returns all names that may be authenticated. :rtype: `list` of `str` """ def deploy_cert(domain, cert_path, key_path, chain_path, fullchain_path): """Deploy certificate. :param str domain: domain to deploy certificate file :param str cert_path: absolute path to the certificate file :param str key_path: absolute path to the private key file :param str chain_path: absolute path to the certificate chain file :param str fullchain_path: absolute path to the certificate fullchain file (cert plus chain) :raises .PluginError: when cert cannot be deployed """ def enhance(domain, enhancement, options=None): """Perform a configuration enhancement. :param str domain: domain for which to provide enhancement :param str enhancement: An enhancement as defined in :const:`~letsencrypt.constants.ENHANCEMENTS` :param options: Flexible options parameter for enhancement. Check documentation of :const:`~letsencrypt.constants.ENHANCEMENTS` for expected options for each enhancement. :raises .PluginError: If Enhancement is not supported, or if an error occurs during the enhancement. """ def supported_enhancements(): """Returns a list of supported enhancements. :returns: supported enhancements which should be a subset of :const:`~letsencrypt.constants.ENHANCEMENTS` :rtype: :class:`list` of :class:`str` """ def get_all_certs_keys(): """Retrieve all certs and keys set in configuration. :returns: tuples with form `[(cert, key, path)]`, where: - `cert` - str path to certificate file - `key` - str path to associated key file - `path` - file path to configuration file :rtype: list """ def save(title=None, temporary=False): """Saves all changes to the configuration files. Both title and temporary are needed because a save may be intended to be permanent, but the save is not ready to be a full checkpoint. If an exception is raised, it is assumed a new checkpoint was not created. :param str title: The title of the save. If a title is given, the configuration will be saved as a new checkpoint and put in a timestamped directory. `title` has no effect if temporary is true. :param bool temporary: Indicates whether the changes made will be quickly reversed in the future (challenges) :raises .PluginError: when save is unsuccessful """ def rollback_checkpoints(rollback=1): """Revert `rollback` number of configuration checkpoints. :raises .PluginError: when configuration cannot be fully reverted """ def recovery_routine(): """Revert configuration to most recent finalized checkpoint. Remove all changes (temporary and permanent) that have not been finalized. This is useful to protect against crashes and other execution interruptions. :raises .errors.PluginError: If unable to recover the configuration """ def view_config_changes(): """Display all of the LE config changes. :raises .PluginError: when config changes cannot be parsed """ def config_test(): """Make sure the configuration is valid. :raises .MisconfigurationError: when the config is not in a usable state """ def restart(): """Restart or refresh the server content. :raises .PluginError: when server cannot be restarted """ class IDisplay(zope.interface.Interface): """Generic display.""" def notification(message, height, pause): """Displays a string message :param str message: Message to display :param int height: Height of dialog box if applicable :param bool pause: Whether or not the application should pause for confirmation (if available) """ def menu(message, choices, ok_label="OK", # pylint: disable=too-many-arguments cancel_label="Cancel", help_label="", default=None, cli_flag=None): """Displays a generic menu. :param str message: message to display :param choices: choices :type choices: :class:`list` of :func:`tuple` or :class:`str` :param str ok_label: label for OK button :param str cancel_label: label for Cancel button :param str help_label: label for Help button :param int default: default (non-interactive) choice from the menu :param str cli_flag: to automate choice from the menu, eg "--keep" :returns: tuple of (`code`, `index`) where `code` - str display exit code `index` - int index of the user's selection :raises errors.MissingCommandlineFlag: if called in non-interactive mode without a default set """ def input(message, default=None, cli_args=None): """Accept input from the user. :param str message: message to display to the user :returns: tuple of (`code`, `input`) where `code` - str display exit code `input` - str of the user's input :rtype: tuple :raises errors.MissingCommandlineFlag: if called in non-interactive mode without a default set """ def yesno(message, yes_label="Yes", no_label="No", default=None, cli_args=None): """Query the user with a yes/no question. Yes and No label must begin with different letters. :param str message: question for the user :param str default: default (non-interactive) choice from the menu :param str cli_flag: to automate choice from the menu, eg "--redirect / --no-redirect" :returns: True for "Yes", False for "No" :rtype: bool :raises errors.MissingCommandlineFlag: if called in non-interactive mode without a default set """ def checklist(message, tags, default_state, default=None, cli_args=None): """Allow for multiple selections from a menu. :param str message: message to display to the user :param list tags: where each is of type :class:`str` len(tags) > 0 :param bool default_status: If True, items are in a selected state by default. :param str default: default (non-interactive) state of the checklist :param str cli_flag: to automate choice from the menu, eg "--domains" :returns: tuple of the form (code, list_tags) where `code` - int display exit code `list_tags` - list of str tags selected by the user :rtype: tuple :raises errors.MissingCommandlineFlag: if called in non-interactive mode without a default set """ class IValidator(zope.interface.Interface): """Configuration validator.""" def certificate(cert, name, alt_host=None, port=443): """Verifies the certificate presented at name is cert :param OpenSSL.crypto.X509 cert: Expected certificate :param str name: Server's domain name :param bytes alt_host: Host to connect to instead of the IP address of host :param int port: Port to connect to :returns: True if the certificate was verified successfully :rtype: bool """ def redirect(name, port=80, headers=None): """Verify redirect to HTTPS :param str name: Server's domain name :param int port: Port to connect to :param dict headers: HTTP headers to include in request :returns: True if redirect is successfully enabled :rtype: bool """ def hsts(name): """Verify HSTS header is enabled :param str name: Server's domain name :returns: True if HSTS header is successfully enabled :rtype: bool """ def ocsp_stapling(name): """Verify ocsp stapling for domain :param str name: Server's domain name :returns: True if ocsp stapling is successfully enabled :rtype: bool """ class IReporter(zope.interface.Interface): """Interface to collect and display information to the user.""" HIGH_PRIORITY = zope.interface.Attribute( "Used to denote high priority messages") MEDIUM_PRIORITY = zope.interface.Attribute( "Used to denote medium priority messages") LOW_PRIORITY = zope.interface.Attribute( "Used to denote low priority messages") def add_message(self, msg, priority, on_crash=True): """Adds msg to the list of messages to be printed. :param str msg: Message to be displayed to the user. :param int priority: One of HIGH_PRIORITY, MEDIUM_PRIORITY, or LOW_PRIORITY. :param bool on_crash: Whether or not the message should be printed if the program exits abnormally. """ def print_messages(self): """Prints messages to the user and clears the message queue.""" letsencrypt-0.4.1/letsencrypt/notify.py0000644000175000017500000000205612665157707017745 0ustar bmwbmw00000000000000"""Send e-mail notification to system administrators.""" import email import smtplib import socket import subprocess def notify(subject, whom, what): """Send email notification. Try to notify the addressee (``whom``) by e-mail, with Subject: defined by ``subject`` and message body by ``what``. """ msg = email.message_from_string(what) msg.add_header("From", "Let's Encrypt renewal agent ") msg.add_header("To", whom) msg.add_header("Subject", subject) msg = msg.as_string() try: lmtp = smtplib.LMTP() lmtp.connect() lmtp.sendmail("root", [whom], msg) except (smtplib.SMTPHeloError, smtplib.SMTPRecipientsRefused, smtplib.SMTPSenderRefused, smtplib.SMTPDataError, socket.error): # We should try using /usr/sbin/sendmail in this case try: proc = subprocess.Popen(["/usr/sbin/sendmail", "-t"], stdin=subprocess.PIPE) proc.communicate(msg) except OSError: return False return True letsencrypt-0.4.1/letsencrypt/error_handler.py0000644000175000017500000000761712665157707021273 0ustar bmwbmw00000000000000"""Registers functions to be called if an exception or signal occurs.""" import functools import logging import os import signal import traceback logger = logging.getLogger(__name__) # _SIGNALS stores the signals that will be handled by the ErrorHandler. These # signals were chosen as their default handler terminates the process and could # potentially occur from inside Python. Signals such as SIGILL were not # included as they could be a sign of something devious and we should terminate # immediately. _SIGNALS = ([signal.SIGTERM] if os.name == "nt" else [signal.SIGTERM, signal.SIGHUP, signal.SIGQUIT, signal.SIGXCPU, signal.SIGXFSZ]) class ErrorHandler(object): """Registers functions to be called if an exception or signal occurs. This class allows you to register functions that will be called when an exception (excluding SystemExit) or signal is encountered. The class works best as a context manager. For example: with ErrorHandler(cleanup_func): do_something() If an exception is raised out of do_something, cleanup_func will be called. The exception is not caught by the ErrorHandler. Similarly, if a signal is encountered, cleanup_func is called followed by the previously registered signal handler. Every registered function is attempted to be run to completion exactly once. If a registered function raises an exception, it is logged and the next function is called. If a (different) handled signal occurs while calling a registered function, it is attempted to be called again by the next signal handler. """ def __init__(self, func=None, *args, **kwargs): self.funcs = [] self.prev_handlers = {} if func is not None: self.register(func, *args, **kwargs) def __enter__(self): self.set_signal_handlers() def __exit__(self, exec_type, exec_value, trace): # SystemExit is ignored to properly handle forks that don't exec if exec_type not in (None, SystemExit): logger.debug("Encountered exception:\n%s", "".join( traceback.format_exception(exec_type, exec_value, trace))) self.call_registered() self.reset_signal_handlers() def register(self, func, *args, **kwargs): """Sets func to be called with *args and **kwargs during cleanup :param function func: function to be called in case of an error """ self.funcs.append(functools.partial(func, *args, **kwargs)) def call_registered(self): """Calls all registered functions""" logger.debug("Calling registered functions") while self.funcs: try: self.funcs[-1]() except Exception as error: # pylint: disable=broad-except logger.error("Encountered exception during recovery") logger.exception(error) self.funcs.pop() def set_signal_handlers(self): """Sets signal handlers for signals in _SIGNALS.""" for signum in _SIGNALS: prev_handler = signal.getsignal(signum) # If prev_handler is None, the handler was set outside of Python if prev_handler is not None: self.prev_handlers[signum] = prev_handler signal.signal(signum, self._signal_handler) def reset_signal_handlers(self): """Resets signal handlers for signals in _SIGNALS.""" for signum in self.prev_handlers: signal.signal(signum, self.prev_handlers[signum]) self.prev_handlers.clear() def _signal_handler(self, signum, unused_frame): """Calls registered functions and the previous signal handler. :param int signum: number of current signal """ logger.debug("Singal %s encountered", signum) self.call_registered() signal.signal(signum, self.prev_handlers[signum]) os.kill(os.getpid(), signum) letsencrypt-0.4.1/letsencrypt/reverter.py0000644000175000017500000005056212665157707020300 0ustar bmwbmw00000000000000"""Reverter class saves configuration checkpoints and allows for recovery.""" import csv import logging import os import shutil import time import zope.component from letsencrypt import constants from letsencrypt import errors from letsencrypt import interfaces from letsencrypt import le_util from letsencrypt.display import util as display_util logger = logging.getLogger(__name__) class Reverter(object): """Reverter Class - save and revert configuration checkpoints. .. note:: Consider moving everything over to CSV format. :param config: Configuration. :type config: :class:`letsencrypt.interfaces.IConfig` """ def __init__(self, config): self.config = config le_util.make_or_verify_dir( config.backup_dir, constants.CONFIG_DIRS_MODE, os.geteuid(), self.config.strict_permissions) def revert_temporary_config(self): """Reload users original configuration files after a temporary save. This function should reinstall the users original configuration files for all saves with temporary=True :raises .ReverterError: when unable to revert config """ if os.path.isdir(self.config.temp_checkpoint_dir): try: self._recover_checkpoint(self.config.temp_checkpoint_dir) except errors.ReverterError: # We have a partial or incomplete recovery logger.fatal("Incomplete or failed recovery for %s", self.config.temp_checkpoint_dir) raise errors.ReverterError("Unable to revert temporary config") def rollback_checkpoints(self, rollback=1): """Revert 'rollback' number of configuration checkpoints. :param int rollback: Number of checkpoints to reverse. A str num will be cast to an integer. So "2" is also acceptable. :raises .ReverterError: if there is a problem with the input or if the function is unable to correctly revert the configuration checkpoints """ try: rollback = int(rollback) except ValueError: logger.error("Rollback argument must be a positive integer") raise errors.ReverterError("Invalid Input") # Sanity check input if rollback < 0: logger.error("Rollback argument must be a positive integer") raise errors.ReverterError("Invalid Input") backups = os.listdir(self.config.backup_dir) backups.sort() if not backups: logger.warning( "Let's Encrypt hasn't modified your configuration, so rollback " "isn't available.") elif len(backups) < rollback: logger.warning("Unable to rollback %d checkpoints, only %d exist", rollback, len(backups)) while rollback > 0 and backups: cp_dir = os.path.join(self.config.backup_dir, backups.pop()) try: self._recover_checkpoint(cp_dir) except errors.ReverterError: logger.fatal("Failed to load checkpoint during rollback") raise errors.ReverterError( "Unable to load checkpoint during rollback") rollback -= 1 def view_config_changes(self, for_logging=False): """Displays all saved checkpoints. All checkpoints are printed by :meth:`letsencrypt.interfaces.IDisplay.notification`. .. todo:: Decide on a policy for error handling, OSError IOError... :raises .errors.ReverterError: If invalid directory structure. """ backups = os.listdir(self.config.backup_dir) backups.sort(reverse=True) if not backups: logger.info("The Let's Encrypt client has not saved any backups " "of your configuration") return # Make sure there isn't anything unexpected in the backup folder # There should only be timestamped (float) directories try: for bkup in backups: float(bkup) except ValueError: raise errors.ReverterError( "Invalid directories in {0}".format(self.config.backup_dir)) output = [] for bkup in backups: output.append(time.ctime(float(bkup))) cur_dir = os.path.join(self.config.backup_dir, bkup) with open(os.path.join(cur_dir, "CHANGES_SINCE")) as changes_fd: output.append(changes_fd.read()) output.append("Affected files:") with open(os.path.join(cur_dir, "FILEPATHS")) as paths_fd: filepaths = paths_fd.read().splitlines() for path in filepaths: output.append(" {0}".format(path)) if os.path.isfile(os.path.join(cur_dir, "NEW_FILES")): with open(os.path.join(cur_dir, "NEW_FILES")) as new_fd: output.append("New Configuration Files:") filepaths = new_fd.read().splitlines() for path in filepaths: output.append(" {0}".format(path)) output.append(os.linesep) if for_logging: return os.linesep.join(output) zope.component.getUtility(interfaces.IDisplay).notification( os.linesep.join(output), display_util.HEIGHT) def add_to_temp_checkpoint(self, save_files, save_notes): """Add files to temporary checkpoint. :param set save_files: set of filepaths to save :param str save_notes: notes about changes during the save """ self._add_to_checkpoint_dir( self.config.temp_checkpoint_dir, save_files, save_notes) def add_to_checkpoint(self, save_files, save_notes): """Add files to a permanent checkpoint. :param set save_files: set of filepaths to save :param str save_notes: notes about changes during the save """ # Check to make sure we are not overwriting a temp file self._check_tempfile_saves(save_files) self._add_to_checkpoint_dir( self.config.in_progress_dir, save_files, save_notes) def _add_to_checkpoint_dir(self, cp_dir, save_files, save_notes): """Add save files to checkpoint directory. :param str cp_dir: Checkpoint directory filepath :param set save_files: set of files to save :param str save_notes: notes about changes made during the save :raises IOError: if unable to open cp_dir + FILEPATHS file :raises .ReverterError: if unable to add checkpoint """ le_util.make_or_verify_dir( cp_dir, constants.CONFIG_DIRS_MODE, os.geteuid(), self.config.strict_permissions) op_fd, existing_filepaths = self._read_and_append( os.path.join(cp_dir, "FILEPATHS")) idx = len(existing_filepaths) for filename in save_files: # No need to copy/index already existing files # The oldest copy already exists in the directory... if filename not in existing_filepaths: # Tag files with index so multiple files can # have the same filename logger.debug("Creating backup of %s", filename) try: shutil.copy2(filename, os.path.join( cp_dir, os.path.basename(filename) + "_" + str(idx))) op_fd.write(filename + os.linesep) # http://stackoverflow.com/questions/4726260/effective-use-of-python-shutil-copy2 except IOError: op_fd.close() logger.error( "Unable to add file %s to checkpoint %s", filename, cp_dir) raise errors.ReverterError( "Unable to add file {0} to checkpoint " "{1}".format(filename, cp_dir)) idx += 1 op_fd.close() with open(os.path.join(cp_dir, "CHANGES_SINCE"), "a") as notes_fd: notes_fd.write(save_notes) def _read_and_append(self, filepath): # pylint: disable=no-self-use """Reads the file lines and returns a file obj. Read the file returning the lines, and a pointer to the end of the file. """ # Open up filepath differently depending on if it already exists if os.path.isfile(filepath): op_fd = open(filepath, "r+") lines = op_fd.read().splitlines() else: lines = [] op_fd = open(filepath, "w") return op_fd, lines def _recover_checkpoint(self, cp_dir): """Recover a specific checkpoint. Recover a specific checkpoint provided by cp_dir Note: this function does not reload augeas. :param str cp_dir: checkpoint directory file path :raises errors.ReverterError: If unable to recover checkpoint """ # Undo all commands if os.path.isfile(os.path.join(cp_dir, "COMMANDS")): self._run_undo_commands(os.path.join(cp_dir, "COMMANDS")) # Revert all changed files if os.path.isfile(os.path.join(cp_dir, "FILEPATHS")): try: with open(os.path.join(cp_dir, "FILEPATHS")) as paths_fd: filepaths = paths_fd.read().splitlines() for idx, path in enumerate(filepaths): shutil.copy2(os.path.join( cp_dir, os.path.basename(path) + "_" + str(idx)), path) except (IOError, OSError): # This file is required in all checkpoints. logger.error("Unable to recover files from %s", cp_dir) raise errors.ReverterError( "Unable to recover files from %s" % cp_dir) # Remove any newly added files if they exist self._remove_contained_files(os.path.join(cp_dir, "NEW_FILES")) try: shutil.rmtree(cp_dir) except OSError: logger.error("Unable to remove directory: %s", cp_dir) raise errors.ReverterError( "Unable to remove directory: %s" % cp_dir) def _run_undo_commands(self, filepath): # pylint: disable=no-self-use """Run all commands in a file.""" with open(filepath, 'rb') as csvfile: csvreader = csv.reader(csvfile) for command in reversed(list(csvreader)): try: le_util.run_script(command) except errors.SubprocessError: logger.error( "Unable to run undo command: %s", " ".join(command)) def _check_tempfile_saves(self, save_files): """Verify save isn't overwriting any temporary files. :param set save_files: Set of files about to be saved. :raises letsencrypt.errors.ReverterError: when save is attempting to overwrite a temporary file. """ protected_files = [] # Get temp modified files temp_path = os.path.join(self.config.temp_checkpoint_dir, "FILEPATHS") if os.path.isfile(temp_path): with open(temp_path, "r") as protected_fd: protected_files.extend(protected_fd.read().splitlines()) # Get temp new files new_path = os.path.join(self.config.temp_checkpoint_dir, "NEW_FILES") if os.path.isfile(new_path): with open(new_path, "r") as protected_fd: protected_files.extend(protected_fd.read().splitlines()) # Verify no save_file is in protected_files for filename in protected_files: if filename in save_files: raise errors.ReverterError( "Attempting to overwrite challenge " "file - %s" % filename) def register_file_creation(self, temporary, *files): r"""Register the creation of all files during letsencrypt execution. Call this method before writing to the file to make sure that the file will be cleaned up if the program exits unexpectedly. (Before a save occurs) :param bool temporary: If the file creation registry is for a temp or permanent save. :param \*files: file paths (str) to be registered :raises letsencrypt.errors.ReverterError: If call does not contain necessary parameters or if the file creation is unable to be registered. """ # Make sure some files are provided... as this is an error # Made this mistake in my initial implementation of apache.dvsni.py if not files: raise errors.ReverterError( "Forgot to provide files to registration call") cp_dir = self._get_cp_dir(temporary) # Append all new files (that aren't already registered) new_fd = None try: new_fd, ex_files = self._read_and_append( os.path.join(cp_dir, "NEW_FILES")) for path in files: if path not in ex_files: new_fd.write("{0}{1}".format(path, os.linesep)) except (IOError, OSError): logger.error("Unable to register file creation(s) - %s", files) raise errors.ReverterError( "Unable to register file creation(s) - {0}".format(files)) finally: if new_fd is not None: new_fd.close() def register_undo_command(self, temporary, command): """Register a command to be run to undo actions taken. .. warning:: This function does not enforce order of operations in terms of file modification vs. command registration. All undo commands are run first before all normal files are reverted to their previous state. If you need to maintain strict order, you may create checkpoints before and after the the command registration. This function may be improved in the future based on demand. :param bool temporary: Whether the command should be saved in the IN_PROGRESS or TEMPORARY checkpoints. :param command: Command to be run. :type command: list of str """ commands_fp = os.path.join(self._get_cp_dir(temporary), "COMMANDS") command_file = None try: if os.path.isfile(commands_fp): command_file = open(commands_fp, "ab") else: command_file = open(commands_fp, "wb") csvwriter = csv.writer(command_file) csvwriter.writerow(command) except (IOError, OSError): logger.error("Unable to register undo command") raise errors.ReverterError( "Unable to register undo command.") finally: if command_file is not None: command_file.close() def _get_cp_dir(self, temporary): """Return the proper reverter directory.""" if temporary: cp_dir = self.config.temp_checkpoint_dir else: cp_dir = self.config.in_progress_dir le_util.make_or_verify_dir( cp_dir, constants.CONFIG_DIRS_MODE, os.geteuid(), self.config.strict_permissions) return cp_dir def recovery_routine(self): """Revert configuration to most recent finalized checkpoint. Remove all changes (temporary and permanent) that have not been finalized. This is useful to protect against crashes and other execution interruptions. :raises .errors.ReverterError: If unable to recover the configuration """ # First, any changes found in IConfig.temp_checkpoint_dir are removed, # then IN_PROGRESS changes are removed The order is important. # IN_PROGRESS is unable to add files that are already added by a TEMP # change. Thus TEMP must be rolled back first because that will be the # 'latest' occurrence of the file. self.revert_temporary_config() if os.path.isdir(self.config.in_progress_dir): try: self._recover_checkpoint(self.config.in_progress_dir) except errors.ReverterError: # We have a partial or incomplete recovery logger.fatal("Incomplete or failed recovery for IN_PROGRESS " "checkpoint - %s", self.config.in_progress_dir) raise errors.ReverterError( "Incomplete or failed recovery for IN_PROGRESS checkpoint " "- %s" % self.config.in_progress_dir) def _remove_contained_files(self, file_list): # pylint: disable=no-self-use """Erase all files contained within file_list. :param str file_list: file containing list of file paths to be deleted :returns: Success :rtype: bool :raises letsencrypt.errors.ReverterError: If all files within file_list cannot be removed """ # Check to see that file exists to differentiate can't find file_list # and can't remove filepaths within file_list errors. if not os.path.isfile(file_list): return False try: with open(file_list, "r") as list_fd: filepaths = list_fd.read().splitlines() for path in filepaths: # Files are registered before they are added... so # check to see if file exists first if os.path.lexists(path): os.remove(path) else: logger.warning( "File: %s - Could not be found to be deleted %s - " "LE probably shut down unexpectedly", os.linesep, path) except (IOError, OSError): logger.fatal( "Unable to remove filepaths contained within %s", file_list) raise errors.ReverterError( "Unable to remove filepaths contained within " "{0}".format(file_list)) return True def finalize_checkpoint(self, title): """Finalize the checkpoint. Timestamps and permanently saves all changes made through the use of :func:`~add_to_checkpoint` and :func:`~register_file_creation` :param str title: Title describing checkpoint :raises letsencrypt.errors.ReverterError: when the checkpoint is not able to be finalized. """ # Adds title to self.config.in_progress_dir CHANGES_SINCE # Move self.config.in_progress_dir to Backups directory and # rename the directory as a timestamp # Check to make sure an "in progress" directory exists if not os.path.isdir(self.config.in_progress_dir): return changes_since_path = os.path.join( self.config.in_progress_dir, "CHANGES_SINCE") changes_since_tmp_path = os.path.join( self.config.in_progress_dir, "CHANGES_SINCE.tmp") try: with open(changes_since_tmp_path, "w") as changes_tmp: changes_tmp.write("-- %s --\n" % title) with open(changes_since_path, "r") as changes_orig: changes_tmp.write(changes_orig.read()) shutil.move(changes_since_tmp_path, changes_since_path) except (IOError, OSError): logger.error("Unable to finalize checkpoint - adding title") raise errors.ReverterError("Unable to add title") self._timestamp_progress_dir() def _timestamp_progress_dir(self): """Timestamp the checkpoint.""" # It is possible save checkpoints faster than 1 per second resulting in # collisions in the naming convention. cur_time = time.time() for _ in xrange(10): final_dir = os.path.join(self.config.backup_dir, str(cur_time)) try: os.rename(self.config.in_progress_dir, final_dir) return except OSError: # It is possible if the checkpoints are made extremely quickly # that will result in a name collision. # If so, increment and try again cur_time += .01 # After 10 attempts... something is probably wrong here... logger.error( "Unable to finalize checkpoint, %s -> %s", self.config.in_progress_dir, final_dir) raise errors.ReverterError( "Unable to finalize checkpoint renaming") letsencrypt-0.4.1/letsencrypt/log.py0000644000175000017500000000425112665157707017215 0ustar bmwbmw00000000000000"""Logging utilities.""" import logging import dialog from letsencrypt.display import util as display_util class DialogHandler(logging.Handler): # pylint: disable=too-few-public-methods """Logging handler using dialog info box. :ivar int height: Height of the info box (without padding). :ivar int width: Width of the info box (without padding). :ivar list lines: Lines to be displayed in the info box. :ivar d: Instance of :class:`dialog.Dialog`. """ PADDING_HEIGHT = 2 PADDING_WIDTH = 4 def __init__(self, level=logging.NOTSET, height=display_util.HEIGHT, width=display_util.WIDTH - 4, d=None): # Handler not new-style -> no super logging.Handler.__init__(self, level) self.height = height self.width = width # "dialog" collides with module name... self.d = dialog.Dialog() if d is None else d self.lines = [] def emit(self, record): """Emit message to a dialog info box. Only show the last (self.height) lines; note that lines can wrap at self.width, so a single line could actually be multiple lines. """ for line in self.format(record).splitlines(): # check for lines that would wrap cur_out = line while len(cur_out) > self.width: # find first space before self.width chars into cur_out last_space_pos = cur_out.rfind(' ', 0, self.width) if last_space_pos == -1: # no spaces, just cut them off at whatever self.lines.append(cur_out[0:self.width]) cur_out = cur_out[self.width:] else: # cut off at last space self.lines.append(cur_out[0:last_space_pos]) cur_out = cur_out[last_space_pos + 1:] if cur_out != '': self.lines.append(cur_out) # show last 16 lines content = '\n'.join(self.lines[-self.height:]) # add the padding around the box self.d.infobox( content, self.height + self.PADDING_HEIGHT, self.width + self.PADDING_WIDTH) letsencrypt-0.4.1/letsencrypt/tests/0000755000175000017500000000000012665157717017223 5ustar bmwbmw00000000000000letsencrypt-0.4.1/letsencrypt/tests/cli_test.py0000644000175000017500000013276012665157707021413 0ustar bmwbmw00000000000000"""Tests for letsencrypt.cli.""" import argparse import functools import itertools import os import shutil import StringIO import traceback import tempfile import unittest import mock from acme import jose from letsencrypt import account from letsencrypt import cli from letsencrypt import configuration from letsencrypt import constants from letsencrypt import crypto_util from letsencrypt import errors from letsencrypt import le_util from letsencrypt import storage from letsencrypt.plugins import disco from letsencrypt.plugins import manual from letsencrypt.tests import storage_test from letsencrypt.tests import test_util CERT = test_util.vector_path('cert.pem') CSR = test_util.vector_path('csr.der') KEY = test_util.vector_path('rsa256_key.pem') class CLITest(unittest.TestCase): # pylint: disable=too-many-public-methods """Tests for different commands.""" def setUp(self): self.tmp_dir = tempfile.mkdtemp() self.config_dir = os.path.join(self.tmp_dir, 'config') self.work_dir = os.path.join(self.tmp_dir, 'work') self.logs_dir = os.path.join(self.tmp_dir, 'logs') self.standard_args = ['--config-dir', self.config_dir, '--work-dir', self.work_dir, '--logs-dir', self.logs_dir, '--text'] def tearDown(self): shutil.rmtree(self.tmp_dir) def _call(self, args): "Run the cli with output streams and actual client mocked out" with mock.patch('letsencrypt.cli.client') as client: ret, stdout, stderr = self._call_no_clientmock(args) return ret, stdout, stderr, client def _call_no_clientmock(self, args): "Run the client with output streams mocked out" args = self.standard_args + args with mock.patch('letsencrypt.cli.sys.stdout') as stdout: with mock.patch('letsencrypt.cli.sys.stderr') as stderr: ret = cli.main(args[:]) # NOTE: parser can alter its args! return ret, stdout, stderr def _call_stdout(self, args): """ Variant of _call that preserves stdout so that it can be mocked by the caller. """ args = self.standard_args + args with mock.patch('letsencrypt.cli.sys.stderr') as stderr: with mock.patch('letsencrypt.cli.client') as client: ret = cli.main(args[:]) # NOTE: parser can alter its args! return ret, None, stderr, client def test_no_flags(self): with MockedVerb("run") as mock_run: self._call([]) self.assertEqual(1, mock_run.call_count) def _help_output(self, args): "Run a command, and return the ouput string for scrutiny" output = StringIO.StringIO() with mock.patch('letsencrypt.cli.sys.stdout', new=output): self.assertRaises(SystemExit, self._call_stdout, args) out = output.getvalue() return out def test_help(self): self.assertRaises(SystemExit, self._call, ['--help']) self.assertRaises(SystemExit, self._call, ['--help', 'all']) plugins = disco.PluginsRegistry.find_all() out = self._help_output(['--help', 'all']) self.assertTrue("--configurator" in out) self.assertTrue("how a cert is deployed" in out) self.assertTrue("--manual-test-mode" in out) out = self._help_output(['-h', 'nginx']) if "nginx" in plugins: # may be false while building distributions without plugins self.assertTrue("--nginx-ctl" in out) self.assertTrue("--manual-test-mode" not in out) self.assertTrue("--checkpoints" not in out) out = self._help_output(['-h']) self.assertTrue("letsencrypt-auto" not in out) # test cli.cli_command if "nginx" in plugins: self.assertTrue("Use the Nginx plugin" in out) else: self.assertTrue("(nginx support is experimental" in out) out = self._help_output(['--help', 'plugins']) self.assertTrue("--manual-test-mode" not in out) self.assertTrue("--prepare" in out) self.assertTrue("Plugin options" in out) out = self._help_output(['--help', 'install']) self.assertTrue("--cert-path" in out) self.assertTrue("--key-path" in out) out = self._help_output(['--help', 'revoke']) self.assertTrue("--cert-path" in out) self.assertTrue("--key-path" in out) out = self._help_output(['-h', 'config_changes']) self.assertTrue("--cert-path" not in out) self.assertTrue("--key-path" not in out) out = self._help_output(['-h']) self.assertTrue(cli.usage_strings(plugins)[0] in out) def _cli_missing_flag(self, args, message): "Ensure that a particular error raises a missing cli flag error containing message" exc = None try: with mock.patch('letsencrypt.cli.sys.stderr'): cli.main(self.standard_args + args[:]) # NOTE: parser can alter its args! except errors.MissingCommandlineFlag as exc: self.assertTrue(message in str(exc)) self.assertTrue(exc is not None) def test_noninteractive(self): args = ['-n', 'certonly'] self._cli_missing_flag(args, "specify a plugin") args.extend(['--standalone', '-d', 'eg.is']) self._cli_missing_flag(args, "register before running") with mock.patch('letsencrypt.cli._auth_from_domains'): with mock.patch('letsencrypt.cli.client.acme_from_config_key'): args.extend(['--email', 'io@io.is']) self._cli_missing_flag(args, "--agree-tos") @mock.patch('letsencrypt.cli.client.acme_client.Client') @mock.patch('letsencrypt.cli._determine_account') @mock.patch('letsencrypt.cli.client.Client.obtain_and_enroll_certificate') @mock.patch('letsencrypt.cli._auth_from_domains') def test_user_agent(self, afd, _obt, det, _client): # Normally the client is totally mocked out, but here we need more # arguments to automate it... args = ["--standalone", "certonly", "-m", "none@none.com", "-d", "example.com", '--agree-tos'] + self.standard_args det.return_value = mock.MagicMock(), None afd.return_value = mock.MagicMock(), "newcert" with mock.patch('letsencrypt.cli.client.acme_client.ClientNetwork') as acme_net: self._call_no_clientmock(args) os_ver = " ".join(le_util.get_os_info()) ua = acme_net.call_args[1]["user_agent"] self.assertTrue(os_ver in ua) import platform plat = platform.platform() if "linux" in plat.lower(): self.assertTrue(platform.linux_distribution()[0] in ua) with mock.patch('letsencrypt.cli.client.acme_client.ClientNetwork') as acme_net: ua = "bandersnatch" args += ["--user-agent", ua] self._call_no_clientmock(args) acme_net.assert_called_once_with(mock.ANY, verify_ssl=True, user_agent=ua) def test_install_abspath(self): cert = 'cert' key = 'key' chain = 'chain' fullchain = 'fullchain' with MockedVerb('install') as mock_install: self._call(['install', '--cert-path', cert, '--key-path', 'key', '--chain-path', 'chain', '--fullchain-path', 'fullchain']) args = mock_install.call_args[0][0] self.assertEqual(args.cert_path, os.path.abspath(cert)) self.assertEqual(args.key_path, os.path.abspath(key)) self.assertEqual(args.chain_path, os.path.abspath(chain)) self.assertEqual(args.fullchain_path, os.path.abspath(fullchain)) @mock.patch('letsencrypt.cli.record_chosen_plugins') @mock.patch('letsencrypt.cli.display_ops') def test_installer_selection(self, mock_display_ops, _rec): self._call(['install', '--domains', 'foo.bar', '--cert-path', 'cert', '--key-path', 'key', '--chain-path', 'chain']) self.assertEqual(mock_display_ops.pick_installer.call_count, 1) @mock.patch('letsencrypt.le_util.exe_exists') def test_configurator_selection(self, mock_exe_exists): mock_exe_exists.return_value = True real_plugins = disco.PluginsRegistry.find_all() args = ['--apache', '--authenticator', 'standalone'] # This needed two calls to find_all(), which we're avoiding for now # because of possible side effects: # https://github.com/letsencrypt/letsencrypt/commit/51ed2b681f87b1eb29088dd48718a54f401e4855 #with mock.patch('letsencrypt.cli.plugins_testable') as plugins: # plugins.return_value = {"apache": True, "nginx": True} # ret, _, _, _ = self._call(args) # self.assertTrue("Too many flags setting" in ret) args = ["install", "--nginx", "--cert-path", "/tmp/blah", "--key-path", "/tmp/blah", "--nginx-server-root", "/nonexistent/thing", "-d", "example.com", "--debug"] if "nginx" in real_plugins: # Sending nginx a non-existent conf dir will simulate misconfiguration # (we can only do that if letsencrypt-nginx is actually present) ret, _, _, _ = self._call(args) self.assertTrue("The nginx plugin is not working" in ret) self.assertTrue("MisconfigurationError" in ret) args = ["certonly", "--webroot"] try: self._call(args) assert False, "Exception should have been raised" except errors.PluginSelectionError as e: self.assertTrue("please set either --webroot-path" in e.message) self._cli_missing_flag(["--standalone"], "With the standalone plugin, you probably") with mock.patch("letsencrypt.cli._init_le_client") as mock_init: with mock.patch("letsencrypt.cli._auth_from_domains") as mock_afd: mock_afd.return_value = (mock.MagicMock(), mock.MagicMock()) self._call(["certonly", "--manual", "-d", "foo.bar"]) unused_config, auth, unused_installer = mock_init.call_args[0] self.assertTrue(isinstance(auth, manual.Authenticator)) with MockedVerb("certonly") as mock_certonly: self._call(["auth", "--standalone"]) self.assertEqual(1, mock_certonly.call_count) def test_rollback(self): _, _, _, client = self._call(['rollback']) self.assertEqual(1, client.rollback.call_count) _, _, _, client = self._call(['rollback', '--checkpoints', '123']) client.rollback.assert_called_once_with( mock.ANY, 123, mock.ANY, mock.ANY) def test_config_changes(self): _, _, _, client = self._call(['config_changes']) self.assertEqual(1, client.view_config_changes.call_count) def test_plugins(self): flags = ['--init', '--prepare', '--authenticators', '--installers'] for args in itertools.chain( *(itertools.combinations(flags, r) for r in xrange(len(flags)))): self._call(['plugins'] + list(args)) @mock.patch('letsencrypt.cli.plugins_disco') @mock.patch('letsencrypt.cli.HelpfulArgumentParser.determine_help_topics') def test_plugins_no_args(self, _det, mock_disco): ifaces = [] plugins = mock_disco.PluginsRegistry.find_all() _, stdout, _, _ = self._call(['plugins']) plugins.visible.assert_called_once_with() plugins.visible().ifaces.assert_called_once_with(ifaces) filtered = plugins.visible().ifaces() stdout.write.called_once_with(str(filtered)) @mock.patch('letsencrypt.cli.plugins_disco') @mock.patch('letsencrypt.cli.HelpfulArgumentParser.determine_help_topics') def test_plugins_init(self, _det, mock_disco): ifaces = [] plugins = mock_disco.PluginsRegistry.find_all() _, stdout, _, _ = self._call(['plugins', '--init']) plugins.visible.assert_called_once_with() plugins.visible().ifaces.assert_called_once_with(ifaces) filtered = plugins.visible().ifaces() self.assertEqual(filtered.init.call_count, 1) filtered.verify.assert_called_once_with(ifaces) verified = filtered.verify() stdout.write.called_once_with(str(verified)) @mock.patch('letsencrypt.cli.plugins_disco') @mock.patch('letsencrypt.cli.HelpfulArgumentParser.determine_help_topics') def test_plugins_prepare(self, _det, mock_disco): ifaces = [] plugins = mock_disco.PluginsRegistry.find_all() _, stdout, _, _ = self._call(['plugins', '--init', '--prepare']) plugins.visible.assert_called_once_with() plugins.visible().ifaces.assert_called_once_with(ifaces) filtered = plugins.visible().ifaces() self.assertEqual(filtered.init.call_count, 1) filtered.verify.assert_called_once_with(ifaces) verified = filtered.verify() verified.prepare.assert_called_once_with() verified.available.assert_called_once_with() available = verified.available() stdout.write.called_once_with(str(available)) def test_certonly_abspath(self): cert = 'cert' key = 'key' chain = 'chain' fullchain = 'fullchain' with MockedVerb('certonly') as mock_obtaincert: self._call(['certonly', '--cert-path', cert, '--key-path', 'key', '--chain-path', 'chain', '--fullchain-path', 'fullchain']) config, unused_plugins = mock_obtaincert.call_args[0] self.assertEqual(config.cert_path, os.path.abspath(cert)) self.assertEqual(config.key_path, os.path.abspath(key)) self.assertEqual(config.chain_path, os.path.abspath(chain)) self.assertEqual(config.fullchain_path, os.path.abspath(fullchain)) def test_certonly_bad_args(self): try: self._call(['-a', 'bad_auth', 'certonly']) assert False, "Exception should have been raised" except errors.PluginSelectionError as e: self.assertTrue('The requested bad_auth plugin does not appear' in e.message) def test_check_config_sanity_domain(self): # Punycode self.assertRaises(errors.ConfigurationError, self._call, ['-d', 'this.is.xn--ls8h.tld']) # FQDN self.assertRaises(errors.ConfigurationError, self._call, ['-d', 'comma,gotwrong.tld']) # FQDN 2 self.assertRaises(errors.ConfigurationError, self._call, ['-d', 'illegal.character=.tld']) # Wildcard self.assertRaises(errors.ConfigurationError, self._call, ['-d', '*.wildcard.tld']) # Bare IP address (this is actually a different error message now) self.assertRaises(errors.ConfigurationError, self._call, ['-d', '204.11.231.35']) def test_run_with_csr(self): # This is an error because you can only use --csr with certonly try: self._call(['--csr', CSR]) except errors.Error as e: assert "Please try the certonly" in e.message return assert False, "Expected supplying --csr to fail with default verb" def _get_argument_parser(self): plugins = disco.PluginsRegistry.find_all() return functools.partial(cli.prepare_and_parse_args, plugins) def test_parse_domains(self): parse = self._get_argument_parser() short_args = ['-d', 'example.com'] namespace = parse(short_args) self.assertEqual(namespace.domains, ['example.com']) short_args = ['-d', 'trailing.period.com.'] namespace = parse(short_args) self.assertEqual(namespace.domains, ['trailing.period.com']) short_args = ['-d', 'example.com,another.net,third.org,example.com'] namespace = parse(short_args) self.assertEqual(namespace.domains, ['example.com', 'another.net', 'third.org']) long_args = ['--domains', 'example.com'] namespace = parse(long_args) self.assertEqual(namespace.domains, ['example.com']) long_args = ['--domains', 'trailing.period.com.'] namespace = parse(long_args) self.assertEqual(namespace.domains, ['trailing.period.com']) long_args = ['--domains', 'example.com,another.net,example.com'] namespace = parse(long_args) self.assertEqual(namespace.domains, ['example.com', 'another.net']) def test_server_flag(self): parse = self._get_argument_parser() namespace = parse('--server example.com'.split()) self.assertEqual(namespace.server, 'example.com') def _check_server_conflict_message(self, parser_args, conflicting_args): parse = self._get_argument_parser() try: parse(parser_args) self.fail( # pragma: no cover "The following flags didn't conflict with " '--server: {0}'.format(', '.join(conflicting_args))) except errors.Error as error: self.assertTrue('--server' in error.message) for arg in conflicting_args: self.assertTrue(arg in error.message) def test_staging_flag(self): parse = self._get_argument_parser() short_args = ['--staging'] namespace = parse(short_args) self.assertTrue(namespace.staging) self.assertEqual(namespace.server, constants.STAGING_URI) short_args += '--server example.com'.split() self._check_server_conflict_message(short_args, '--staging') def _assert_dry_run_flag_worked(self, namespace): self.assertTrue(namespace.dry_run) self.assertTrue(namespace.break_my_certs) self.assertTrue(namespace.staging) self.assertEqual(namespace.server, constants.STAGING_URI) def test_dry_run_flag(self): parse = self._get_argument_parser() short_args = ['--dry-run'] self.assertRaises(errors.Error, parse, short_args) self._assert_dry_run_flag_worked(parse(short_args + ['auth'])) short_args += ['certonly'] self._assert_dry_run_flag_worked(parse(short_args)) short_args += '--server example.com'.split() conflicts = ['--dry-run'] self._check_server_conflict_message(short_args, '--dry-run') short_args += ['--staging'] conflicts += ['--staging'] self._check_server_conflict_message(short_args, conflicts) def _webroot_map_test(self, map_arg, path_arg, domains_arg, # pylint: disable=too-many-arguments expected_map, expectect_domains, extra_args=None): parse = self._get_argument_parser() webroot_map_args = extra_args if extra_args else [] if map_arg: webroot_map_args.extend(["--webroot-map", map_arg]) if path_arg: webroot_map_args.extend(["-w", path_arg]) if domains_arg: webroot_map_args.extend(["-d", domains_arg]) namespace = parse(webroot_map_args) domains = cli._find_domains(namespace, mock.MagicMock()) # pylint: disable=protected-access self.assertEqual(namespace.webroot_map, expected_map) self.assertEqual(set(domains), set(expectect_domains)) def test_parse_webroot(self): parse = self._get_argument_parser() webroot_args = ['--webroot', '-w', '/var/www/example', '-d', 'example.com,www.example.com', '-w', '/var/www/superfluous', '-d', 'superfluo.us', '-d', 'www.superfluo.us'] namespace = parse(webroot_args) self.assertEqual(namespace.webroot_map, { 'example.com': '/var/www/example', 'www.example.com': '/var/www/example', 'www.superfluo.us': '/var/www/superfluous', 'superfluo.us': '/var/www/superfluous'}) webroot_args = ['-d', 'stray.example.com'] + webroot_args self.assertRaises(errors.Error, parse, webroot_args) simple_map = '{"eg.com" : "/tmp"}' expected_map = {"eg.com": "/tmp"} self._webroot_map_test(simple_map, None, None, expected_map, ["eg.com"]) # test merging webroot maps from the cli and a webroot map expected_map["eg2.com"] = "/tmp2" domains = ["eg.com", "eg2.com"] self._webroot_map_test(simple_map, "/tmp2", "eg2.com,eg.com", expected_map, domains) # test inclusion of interactively specified domains in the webroot map with mock.patch('letsencrypt.cli.display_ops.choose_names') as mock_choose: mock_choose.return_value = domains expected_map["eg2.com"] = "/tmp" self._webroot_map_test(None, "/tmp", None, expected_map, domains) extra_args = ['-c', test_util.vector_path('webrootconftest.ini')] self._webroot_map_test(None, None, None, expected_map, domains, extra_args) webroot_map_args = ['--webroot-map', '{"eg.com.,www.eg.com": "/tmp", "eg.is.": "/tmp2"}'] namespace = parse(webroot_map_args) self.assertEqual(namespace.webroot_map, {"eg.com": "/tmp", "www.eg.com": "/tmp", "eg.is": "/tmp2"}) def _certonly_new_request_common(self, mock_client, args=None): with mock.patch('letsencrypt.cli._treat_as_renewal') as mock_renewal: mock_renewal.return_value = ("newcert", None) with mock.patch('letsencrypt.cli._init_le_client') as mock_init: mock_init.return_value = mock_client if args is None: args = [] args += '-d foo.bar -a standalone certonly'.split() self._call(args) @mock.patch('letsencrypt.cli.zope.component.getUtility') def test_certonly_dry_run_new_request_success(self, mock_get_utility): mock_client = mock.MagicMock() mock_client.obtain_and_enroll_certificate.return_value = None self._certonly_new_request_common(mock_client, ['--dry-run']) self.assertEqual( mock_client.obtain_and_enroll_certificate.call_count, 1) self.assertTrue( 'dry run' in mock_get_utility().add_message.call_args[0][0]) # Asserts we don't suggest donating after a successful dry run self.assertEqual(mock_get_utility().add_message.call_count, 1) @mock.patch('letsencrypt.crypto_util.notAfter') @mock.patch('letsencrypt.cli.zope.component.getUtility') def test_certonly_new_request_success(self, mock_get_utility, mock_notAfter): cert_path = '/etc/letsencrypt/live/foo.bar' date = '1970-01-01' mock_notAfter().date.return_value = date mock_lineage = mock.MagicMock(cert=cert_path, fullchain=cert_path) mock_client = mock.MagicMock() mock_client.obtain_and_enroll_certificate.return_value = mock_lineage self._certonly_new_request_common(mock_client) self.assertEqual( mock_client.obtain_and_enroll_certificate.call_count, 1) cert_msg = mock_get_utility().add_message.call_args_list[0][0][0] self.assertTrue(cert_path in cert_msg) self.assertTrue(date in cert_msg) self.assertTrue( 'donate' in mock_get_utility().add_message.call_args[0][0]) def test_certonly_new_request_failure(self): mock_client = mock.MagicMock() mock_client.obtain_and_enroll_certificate.return_value = False self.assertRaises(errors.Error, self._certonly_new_request_common, mock_client) def _test_renewal_common(self, due_for_renewal, extra_args, log_out=None, args=None, renew=True, error_expected=False): # pylint: disable=too-many-locals,too-many-arguments cert_path = 'letsencrypt/tests/testdata/cert.pem' chain_path = '/etc/letsencrypt/live/foo.bar/fullchain.pem' mock_lineage = mock.MagicMock(cert=cert_path, fullchain=chain_path) mock_lineage.should_autorenew.return_value = due_for_renewal mock_certr = mock.MagicMock() mock_key = mock.MagicMock(pem='pem_key') mock_client = mock.MagicMock() mock_client.obtain_certificate.return_value = (mock_certr, 'chain', mock_key, 'csr') try: with mock.patch('letsencrypt.cli._find_duplicative_certs') as mock_fdc: mock_fdc.return_value = (mock_lineage, None) with mock.patch('letsencrypt.cli._init_le_client') as mock_init: mock_init.return_value = mock_client get_utility_path = 'letsencrypt.cli.zope.component.getUtility' with mock.patch(get_utility_path) as mock_get_utility: with mock.patch('letsencrypt.cli.OpenSSL') as mock_ssl: mock_latest = mock.MagicMock() mock_latest.get_issuer.return_value = "Fake fake" mock_ssl.crypto.load_certificate.return_value = mock_latest with mock.patch('letsencrypt.cli.crypto_util'): if not args: args = ['-d', 'isnot.org', '-a', 'standalone', 'certonly'] if extra_args: args += extra_args try: ret, _, _, _ = self._call(args) if ret: print "Returned", ret raise AssertionError(ret) assert not error_expected, "renewal should have errored" except: # pylint: disable=bare-except if not error_expected: raise AssertionError( "Unexpected renewal error:\n" + traceback.format_exc()) if renew: mock_client.obtain_certificate.assert_called_once_with(['isnot.org']) else: self.assertEqual(mock_client.obtain_certificate.call_count, 0) except: self._dump_log() raise finally: if log_out: with open(os.path.join(self.logs_dir, "letsencrypt.log")) as lf: self.assertTrue(log_out in lf.read()) return mock_lineage, mock_get_utility def test_certonly_renewal(self): lineage, get_utility = self._test_renewal_common(True, []) self.assertEqual(lineage.save_successor.call_count, 1) lineage.update_all_links_to.assert_called_once_with( lineage.latest_common_version()) cert_msg = get_utility().add_message.call_args_list[0][0][0] self.assertTrue('fullchain.pem' in cert_msg) self.assertTrue('donate' in get_utility().add_message.call_args[0][0]) def test_certonly_renewal_triggers(self): # --dry-run should force renewal _, get_utility = self._test_renewal_common(False, ['--dry-run', '--keep'], log_out="simulating renewal") self.assertEqual(get_utility().add_message.call_count, 1) self.assertTrue('dry run' in get_utility().add_message.call_args[0][0]) _, _ = self._test_renewal_common(False, ['--renew-by-default', '-tvv', '--debug'], log_out="Auto-renewal forced") self.assertEqual(get_utility().add_message.call_count, 1) _, _ = self._test_renewal_common(False, ['-tvv', '--debug', '--keep'], log_out="not yet due", renew=False) def _dump_log(self): with open(os.path.join(self.logs_dir, "letsencrypt.log")) as lf: print "Logs:" print lf.read() def _make_test_renewal_conf(self, testfile): with open(test_util.vector_path(testfile)) as src: # put the correct path for cert.pem, chain.pem etc in the renewal conf renewal_conf = src.read().replace("MAGICDIR", test_util.vector_path()) rd = os.path.join(self.config_dir, "renewal") if not os.path.exists(rd): os.makedirs(rd) rc = os.path.join(rd, "sample-renewal.conf") with open(rc, "w") as dest: dest.write(renewal_conf) return rc def test_renew_verb(self): self._make_test_renewal_conf('sample-renewal.conf') args = ["renew", "--dry-run", "-tvv"] self._test_renewal_common(True, [], args=args, renew=True) @mock.patch("letsencrypt.cli._set_by_cli") def test_ancient_webroot_renewal_conf(self, mock_set_by_cli): mock_set_by_cli.return_value = False rc_path = self._make_test_renewal_conf('sample-renewal-ancient.conf') args = mock.MagicMock(account=None, email=None, webroot_path=None) config = configuration.NamespaceConfig(args) lineage = storage.RenewableCert(rc_path, configuration.RenewerConfiguration(config)) renewalparams = lineage.configuration["renewalparams"] # pylint: disable=protected-access cli._restore_webroot_config(config, renewalparams) self.assertEqual(config.webroot_path, ["/var/www/"]) def test_renew_verb_empty_config(self): rd = os.path.join(self.config_dir, 'renewal') if not os.path.exists(rd): os.makedirs(rd) with open(os.path.join(rd, 'empty.conf'), 'w'): pass # leave the file empty args = ["renew", "--dry-run", "-tvv"] self._test_renewal_common(False, [], args=args, renew=False, error_expected=True) def _make_dummy_renewal_config(self): renewer_configs_dir = os.path.join(self.config_dir, 'renewal') os.makedirs(renewer_configs_dir) with open(os.path.join(renewer_configs_dir, 'test.conf'), 'w') as f: f.write("My contents don't matter") def _test_renew_common(self, renewalparams=None, error_expected=False, names=None, assert_oc_called=None): self._make_dummy_renewal_config() with mock.patch('letsencrypt.storage.RenewableCert') as mock_rc: mock_lineage = mock.MagicMock() mock_lineage.fullchain = "somepath/fullchain.pem" if renewalparams is not None: mock_lineage.configuration = {'renewalparams': renewalparams} if names is not None: mock_lineage.names.return_value = names mock_rc.return_value = mock_lineage with mock.patch('letsencrypt.cli.obtain_cert') as mock_obtain_cert: self._test_renewal_common(True, None, error_expected=error_expected, args=['renew'], renew=False) if assert_oc_called is not None: if assert_oc_called: self.assertTrue(mock_obtain_cert.called) else: self.assertFalse(mock_obtain_cert.called) def test_renew_no_renewalparams(self): self._test_renew_common(assert_oc_called=False, error_expected=True) def test_renew_no_authenticator(self): self._test_renew_common(renewalparams={}, assert_oc_called=False, error_expected=True) def test_renew_with_bad_int(self): renewalparams = {'authenticator': 'webroot', 'rsa_key_size': 'over 9000'} self._test_renew_common(renewalparams=renewalparams, error_expected=True, assert_oc_called=False) def test_renew_with_bad_domain(self): renewalparams = {'authenticator': 'webroot'} names = ['*.example.com'] self._test_renew_common(renewalparams=renewalparams, error_expected=True, names=names, assert_oc_called=False) def test_renew_plugin_config_restoration(self): renewalparams = {'authenticator': 'webroot', 'webroot_path': 'None', 'webroot_imaginary_flag': '42'} self._test_renew_common(renewalparams=renewalparams, assert_oc_called=True) def test_renew_reconstitute_error(self): # pylint: disable=protected-access with mock.patch('letsencrypt.cli._reconstitute') as mock_reconstitute: mock_reconstitute.side_effect = Exception self._test_renew_common(assert_oc_called=False, error_expected=True) def test_renew_obtain_cert_error(self): self._make_dummy_renewal_config() with mock.patch('letsencrypt.storage.RenewableCert') as mock_rc: mock_lineage = mock.MagicMock() mock_lineage.fullchain = "somewhere/fullchain.pem" mock_rc.return_value = mock_lineage mock_lineage.configuration = { 'renewalparams': {'authenticator': 'webroot'}} with mock.patch('letsencrypt.cli.obtain_cert') as mock_obtain_cert: mock_obtain_cert.side_effect = Exception self._test_renewal_common(True, None, error_expected=True, args=['renew'], renew=False) def test_renew_with_bad_cli_args(self): self._test_renewal_common(True, None, args='renew -d example.com'.split(), renew=False, error_expected=True) self._test_renewal_common(True, None, args='renew --csr {0}'.format(CSR).split(), renew=False, error_expected=True) @mock.patch('letsencrypt.cli.zope.component.getUtility') @mock.patch('letsencrypt.cli._treat_as_renewal') @mock.patch('letsencrypt.cli._init_le_client') def test_certonly_reinstall(self, mock_init, mock_renewal, mock_get_utility): mock_renewal.return_value = ('reinstall', mock.MagicMock()) mock_init.return_value = mock_client = mock.MagicMock() self._call(['-d', 'foo.bar', '-a', 'standalone', 'certonly']) self.assertFalse(mock_client.obtain_certificate.called) self.assertFalse(mock_client.obtain_and_enroll_certificate.called) self.assertEqual(mock_get_utility().add_message.call_count, 0) #self.assertTrue('donate' not in mock_get_utility().add_message.call_args[0][0]) def _test_certonly_csr_common(self, extra_args=None): certr = 'certr' chain = 'chain' mock_client = mock.MagicMock() mock_client.obtain_certificate_from_csr.return_value = (certr, chain) cert_path = '/etc/letsencrypt/live/example.com/cert.pem' mock_client.save_certificate.return_value = cert_path, None, None with mock.patch('letsencrypt.cli._init_le_client') as mock_init: mock_init.return_value = mock_client get_utility_path = 'letsencrypt.cli.zope.component.getUtility' with mock.patch(get_utility_path) as mock_get_utility: chain_path = '/etc/letsencrypt/live/example.com/chain.pem' full_path = '/etc/letsencrypt/live/example.com/fullchain.pem' args = ('-a standalone certonly --csr {0} --cert-path {1} ' '--chain-path {2} --fullchain-path {3}').format( CSR, cert_path, chain_path, full_path).split() if extra_args: args += extra_args with mock.patch('letsencrypt.cli.crypto_util'): self._call(args) if '--dry-run' in args: self.assertFalse(mock_client.save_certificate.called) else: mock_client.save_certificate.assert_called_once_with( certr, chain, cert_path, chain_path, full_path) return mock_get_utility def test_certonly_csr(self): mock_get_utility = self._test_certonly_csr_common() cert_msg = mock_get_utility().add_message.call_args_list[0][0][0] self.assertTrue('cert.pem' in cert_msg) self.assertTrue( 'donate' in mock_get_utility().add_message.call_args[0][0]) def test_certonly_csr_dry_run(self): mock_get_utility = self._test_certonly_csr_common(['--dry-run']) self.assertEqual(mock_get_utility().add_message.call_count, 1) self.assertTrue( 'dry run' in mock_get_utility().add_message.call_args[0][0]) @mock.patch('letsencrypt.cli.client.acme_client') def test_revoke_with_key(self, mock_acme_client): server = 'foo.bar' self._call_no_clientmock(['--cert-path', CERT, '--key-path', KEY, '--server', server, 'revoke']) with open(KEY) as f: mock_acme_client.Client.assert_called_once_with( server, key=jose.JWK.load(f.read()), net=mock.ANY) with open(CERT) as f: cert = crypto_util.pyopenssl_load_certificate(f.read())[0] mock_revoke = mock_acme_client.Client().revoke mock_revoke.assert_called_once_with(jose.ComparableX509(cert)) @mock.patch('letsencrypt.cli._determine_account') def test_revoke_without_key(self, mock_determine_account): mock_determine_account.return_value = (mock.MagicMock(), None) _, _, _, client = self._call(['--cert-path', CERT, 'revoke']) with open(CERT) as f: cert = crypto_util.pyopenssl_load_certificate(f.read())[0] mock_revoke = client.acme_from_config_key().revoke mock_revoke.assert_called_once_with(jose.ComparableX509(cert)) @mock.patch('letsencrypt.cli.sys') def test_handle_exception(self, mock_sys): # pylint: disable=protected-access from acme import messages config = mock.MagicMock() mock_open = mock.mock_open() with mock.patch('letsencrypt.cli.open', mock_open, create=True): exception = Exception('detail') config.verbose_count = 1 cli._handle_exception( Exception, exc_value=exception, trace=None, config=None) mock_open().write.assert_called_once_with(''.join( traceback.format_exception_only(Exception, exception))) error_msg = mock_sys.exit.call_args_list[0][0][0] self.assertTrue('unexpected error' in error_msg) with mock.patch('letsencrypt.cli.open', mock_open, create=True): mock_open.side_effect = [KeyboardInterrupt] error = errors.Error('detail') cli._handle_exception( errors.Error, exc_value=error, trace=None, config=None) # assert_any_call used because sys.exit doesn't exit in cli.py mock_sys.exit.assert_any_call(''.join( traceback.format_exception_only(errors.Error, error))) exception = messages.Error(detail='alpha', typ='urn:acme:error:triffid', title='beta') config = mock.MagicMock(debug=False, verbose_count=-3) cli._handle_exception( messages.Error, exc_value=exception, trace=None, config=config) error_msg = mock_sys.exit.call_args_list[-1][0][0] self.assertTrue('unexpected error' in error_msg) self.assertTrue('acme:error' not in error_msg) self.assertTrue('alpha' in error_msg) self.assertTrue('beta' in error_msg) config = mock.MagicMock(debug=False, verbose_count=1) cli._handle_exception( messages.Error, exc_value=exception, trace=None, config=config) error_msg = mock_sys.exit.call_args_list[-1][0][0] self.assertTrue('unexpected error' in error_msg) self.assertTrue('acme:error' in error_msg) self.assertTrue('alpha' in error_msg) interrupt = KeyboardInterrupt('detail') cli._handle_exception( KeyboardInterrupt, exc_value=interrupt, trace=None, config=None) mock_sys.exit.assert_called_with(''.join( traceback.format_exception_only(KeyboardInterrupt, interrupt))) def test_read_file(self): rel_test_path = os.path.relpath(os.path.join(self.tmp_dir, 'foo')) self.assertRaises( argparse.ArgumentTypeError, cli.read_file, rel_test_path) test_contents = 'bar\n' with open(rel_test_path, 'w') as f: f.write(test_contents) path, contents = cli.read_file(rel_test_path) self.assertEqual(path, os.path.abspath(path)) self.assertEqual(contents, test_contents) def test_agree_dev_preview_config(self): with MockedVerb('run') as mocked_run: self._call(['-c', test_util.vector_path('cli.ini')]) self.assertTrue(mocked_run.called) class DetermineAccountTest(unittest.TestCase): """Tests for letsencrypt.cli._determine_account.""" def setUp(self): self.args = mock.MagicMock(account=None, email=None, register_unsafely_without_email=False) self.config = configuration.NamespaceConfig(self.args) self.accs = [mock.MagicMock(id='x'), mock.MagicMock(id='y')] self.account_storage = account.AccountMemoryStorage() def _call(self): # pylint: disable=protected-access from letsencrypt.cli import _determine_account with mock.patch('letsencrypt.cli.account.AccountFileStorage') as mock_storage: mock_storage.return_value = self.account_storage return _determine_account(self.config) def test_args_account_set(self): self.account_storage.save(self.accs[1]) self.config.account = self.accs[1].id self.assertEqual((self.accs[1], None), self._call()) self.assertEqual(self.accs[1].id, self.config.account) self.assertTrue(self.config.email is None) def test_single_account(self): self.account_storage.save(self.accs[0]) self.assertEqual((self.accs[0], None), self._call()) self.assertEqual(self.accs[0].id, self.config.account) self.assertTrue(self.config.email is None) @mock.patch('letsencrypt.client.display_ops.choose_account') def test_multiple_accounts(self, mock_choose_accounts): for acc in self.accs: self.account_storage.save(acc) mock_choose_accounts.return_value = self.accs[1] self.assertEqual((self.accs[1], None), self._call()) self.assertEqual( set(mock_choose_accounts.call_args[0][0]), set(self.accs)) self.assertEqual(self.accs[1].id, self.config.account) self.assertTrue(self.config.email is None) @mock.patch('letsencrypt.client.display_ops.get_email') def test_no_accounts_no_email(self, mock_get_email): mock_get_email.return_value = 'foo@bar.baz' with mock.patch('letsencrypt.cli.client') as client: client.register.return_value = ( self.accs[0], mock.sentinel.acme) self.assertEqual((self.accs[0], mock.sentinel.acme), self._call()) client.register.assert_called_once_with( self.config, self.account_storage, tos_cb=mock.ANY) self.assertEqual(self.accs[0].id, self.config.account) self.assertEqual('foo@bar.baz', self.config.email) def test_no_accounts_email(self): self.config.email = 'other email' with mock.patch('letsencrypt.cli.client') as client: client.register.return_value = (self.accs[1], mock.sentinel.acme) self._call() self.assertEqual(self.accs[1].id, self.config.account) self.assertEqual('other email', self.config.email) class DuplicativeCertsTest(storage_test.BaseRenewableCertTest): """Test to avoid duplicate lineages.""" def setUp(self): super(DuplicativeCertsTest, self).setUp() self.config.write() self._write_out_ex_kinds() def tearDown(self): shutil.rmtree(self.tempdir) @mock.patch('letsencrypt.le_util.make_or_verify_dir') def test_find_duplicative_names(self, unused_makedir): from letsencrypt.cli import _find_duplicative_certs test_cert = test_util.load_vector('cert-san.pem') with open(self.test_rc.cert, 'w') as f: f.write(test_cert) # No overlap at all result = _find_duplicative_certs( self.cli_config, ['wow.net', 'hooray.org']) self.assertEqual(result, (None, None)) # Totally identical result = _find_duplicative_certs( self.cli_config, ['example.com', 'www.example.com']) self.assertTrue(result[0].configfile.filename.endswith('example.org.conf')) self.assertEqual(result[1], None) # Superset result = _find_duplicative_certs( self.cli_config, ['example.com', 'www.example.com', 'something.new']) self.assertEqual(result[0], None) self.assertTrue(result[1].configfile.filename.endswith('example.org.conf')) # Partial overlap doesn't count result = _find_duplicative_certs( self.cli_config, ['example.com', 'something.new']) self.assertEqual(result, (None, None)) class MockedVerb(object): """Simple class that can be used for mocking out verbs/subcommands. Storing a dictionary of verbs and the functions that implement them in letsencrypt.cli makes mocking much more complicated. This class can be used as a simple context manager for mocking out verbs in CLI tests. For example: with MockedVerb("run") as mock_run: self._call([]) self.assertEqual(1, mock_run.call_count) """ def __init__(self, verb_name): self.verb_dict = cli.HelpfulArgumentParser.VERBS self.verb_func = None self.verb_name = verb_name def __enter__(self): self.verb_func = self.verb_dict[self.verb_name] mocked_func = mock.MagicMock() self.verb_dict[self.verb_name] = mocked_func return mocked_func def __exit__(self, unused_type, unused_value, unused_trace): self.verb_dict[self.verb_name] = self.verb_func if __name__ == '__main__': unittest.main() # pragma: no cover letsencrypt-0.4.1/letsencrypt/tests/errors_test.py0000644000175000017500000000251012665157707022145 0ustar bmwbmw00000000000000"""Tests for letsencrypt.errors.""" import unittest import mock from acme import messages from letsencrypt import achallenges from letsencrypt.tests import acme_util class FaiiledChallengesTest(unittest.TestCase): """Tests for letsencrypt.errors.FailedChallenges.""" def setUp(self): from letsencrypt.errors import FailedChallenges self.error = FailedChallenges(set([achallenges.DNS( domain="example.com", challb=messages.ChallengeBody( chall=acme_util.DNS, uri=None, error=messages.Error(typ="tls", detail="detail")))])) def test_str(self): self.assertTrue(str(self.error).startswith( "Failed authorization procedure. example.com (dns): tls")) class StandaloneBindErrorTest(unittest.TestCase): """Tests for letsencrypt.errors.StandaloneBindError.""" def setUp(self): from letsencrypt.errors import StandaloneBindError self.error = StandaloneBindError(mock.sentinel.error, 1234) def test_instance_args(self): self.assertEqual(mock.sentinel.error, self.error.socket_error) self.assertEqual(1234, self.error.port) def test_str(self): self.assertTrue(str(self.error).startswith( "Problem binding to port 1234: ")) if __name__ == "__main__": unittest.main() # pragma: no cover letsencrypt-0.4.1/letsencrypt/tests/error_handler_test.py0000644000175000017500000000464312665157707023470 0ustar bmwbmw00000000000000"""Tests for letsencrypt.error_handler.""" import signal import sys import unittest import mock class ErrorHandlerTest(unittest.TestCase): """Tests for letsencrypt.error_handler.""" def setUp(self): from letsencrypt import error_handler self.init_func = mock.MagicMock() self.init_args = set((42,)) self.init_kwargs = {'foo': 'bar'} self.handler = error_handler.ErrorHandler(self.init_func, *self.init_args, **self.init_kwargs) # pylint: disable=protected-access self.signals = error_handler._SIGNALS def test_context_manager(self): try: with self.handler: raise ValueError except ValueError: pass self.init_func.assert_called_once_with(*self.init_args, **self.init_kwargs) @mock.patch('letsencrypt.error_handler.os') @mock.patch('letsencrypt.error_handler.signal') def test_signal_handler(self, mock_signal, mock_os): # pylint: disable=protected-access mock_signal.getsignal.return_value = signal.SIG_DFL self.handler.set_signal_handlers() signal_handler = self.handler._signal_handler for signum in self.signals: mock_signal.signal.assert_any_call(signum, signal_handler) signum = self.signals[0] signal_handler(signum, None) self.init_func.assert_called_once_with(*self.init_args, **self.init_kwargs) mock_os.kill.assert_called_once_with(mock_os.getpid(), signum) self.handler.reset_signal_handlers() for signum in self.signals: mock_signal.signal.assert_any_call(signum, signal.SIG_DFL) def test_bad_recovery(self): bad_func = mock.MagicMock(side_effect=[ValueError]) self.handler.register(bad_func) self.handler.call_registered() self.init_func.assert_called_once_with(*self.init_args, **self.init_kwargs) bad_func.assert_called_once_with() def test_sysexit_ignored(self): try: with self.handler: sys.exit(0) except SystemExit: pass self.assertFalse(self.init_func.called) if __name__ == "__main__": unittest.main() # pragma: no cover letsencrypt-0.4.1/letsencrypt/tests/test_util.py0000644000175000017500000000433312665157707021613 0ustar bmwbmw00000000000000"""Test utilities. .. warning:: This module is not part of the public API. """ import os import pkg_resources from cryptography.hazmat.backends import default_backend from cryptography.hazmat.primitives import serialization import OpenSSL from acme import jose def vector_path(*names): """Path to a test vector.""" return pkg_resources.resource_filename( __name__, os.path.join('testdata', *names)) def load_vector(*names): """Load contents of a test vector.""" # luckily, resource_string opens file in binary mode return pkg_resources.resource_string( __name__, os.path.join('testdata', *names)) def _guess_loader(filename, loader_pem, loader_der): _, ext = os.path.splitext(filename) if ext.lower() == '.pem': return loader_pem elif ext.lower() == '.der': return loader_der else: # pragma: no cover raise ValueError("Loader could not be recognized based on extension") def load_cert(*names): """Load certificate.""" loader = _guess_loader( names[-1], OpenSSL.crypto.FILETYPE_PEM, OpenSSL.crypto.FILETYPE_ASN1) return OpenSSL.crypto.load_certificate(loader, load_vector(*names)) def load_comparable_cert(*names): """Load ComparableX509 cert.""" return jose.ComparableX509(load_cert(*names)) def load_csr(*names): """Load certificate request.""" loader = _guess_loader( names[-1], OpenSSL.crypto.FILETYPE_PEM, OpenSSL.crypto.FILETYPE_ASN1) return OpenSSL.crypto.load_certificate_request(loader, load_vector(*names)) def load_comparable_csr(*names): """Load ComparableX509 certificate request.""" return jose.ComparableX509(load_csr(*names)) def load_rsa_private_key(*names): """Load RSA private key.""" loader = _guess_loader(names[-1], serialization.load_pem_private_key, serialization.load_der_private_key) return jose.ComparableRSAKey(loader( load_vector(*names), password=None, backend=default_backend())) def load_pyopenssl_private_key(*names): """Load pyOpenSSL private key.""" loader = _guess_loader( names[-1], OpenSSL.crypto.FILETYPE_PEM, OpenSSL.crypto.FILETYPE_ASN1) return OpenSSL.crypto.load_privatekey(loader, load_vector(*names)) letsencrypt-0.4.1/letsencrypt/tests/__init__.py0000644000175000017500000000003212665157707021326 0ustar bmwbmw00000000000000"""Let's Encrypt Tests""" letsencrypt-0.4.1/letsencrypt/tests/continuity_auth_test.py0000644000175000017500000000404312665157707024062 0ustar bmwbmw00000000000000"""Test for letsencrypt.continuity_auth.""" import unittest import mock from acme import challenges from letsencrypt import achallenges from letsencrypt import errors class PerformTest(unittest.TestCase): """Test client perform function.""" def setUp(self): from letsencrypt.continuity_auth import ContinuityAuthenticator self.auth = ContinuityAuthenticator( mock.MagicMock(server="demo_server.org"), None) self.auth.proof_of_pos.perform = mock.MagicMock( name="proof_of_pos_perform", side_effect=gen_client_resp) def test_pop(self): achalls = [] for i in xrange(4): achalls.append(achallenges.ProofOfPossession( challb=None, domain=str(i))) responses = self.auth.perform(achalls) self.assertEqual(len(responses), 4) for i in xrange(4): self.assertEqual(responses[i], "ProofOfPossession%d" % i) def test_unexpected(self): self.assertRaises( errors.ContAuthError, self.auth.perform, [ achallenges.KeyAuthorizationAnnotatedChallenge( challb=None, domain="0", account_key="invalid_key")]) def test_chall_pref(self): self.assertEqual( self.auth.get_chall_pref("example.com"), [challenges.ProofOfPossession]) class CleanupTest(unittest.TestCase): """Test the Authenticator cleanup function.""" def setUp(self): from letsencrypt.continuity_auth import ContinuityAuthenticator self.auth = ContinuityAuthenticator( mock.MagicMock(server="demo_server.org"), None) def test_unexpected(self): unexpected = achallenges.KeyAuthorizationAnnotatedChallenge( challb=None, domain="0", account_key="dummy_key") self.assertRaises(errors.ContAuthError, self.auth.cleanup, [unexpected]) def gen_client_resp(chall): """Generate a dummy response.""" return "%s%s" % (chall.__class__.__name__, chall.domain) if __name__ == '__main__': unittest.main() # pragma: no cover letsencrypt-0.4.1/letsencrypt/tests/crypto_util_test.py0000644000175000017500000002042412665157707023212 0ustar bmwbmw00000000000000"""Tests for letsencrypt.crypto_util.""" import logging import shutil import tempfile import unittest import OpenSSL import mock import zope.component from letsencrypt import errors from letsencrypt import interfaces from letsencrypt.tests import test_util RSA256_KEY = test_util.load_vector('rsa256_key.pem') RSA512_KEY = test_util.load_vector('rsa512_key.pem') CERT_PATH = test_util.vector_path('cert.pem') CERT = test_util.load_vector('cert.pem') SAN_CERT = test_util.load_vector('cert-san.pem') class InitSaveKeyTest(unittest.TestCase): """Tests for letsencrypt.crypto_util.init_save_key.""" def setUp(self): logging.disable(logging.CRITICAL) zope.component.provideUtility( mock.Mock(strict_permissions=True), interfaces.IConfig) self.key_dir = tempfile.mkdtemp('key_dir') def tearDown(self): logging.disable(logging.NOTSET) shutil.rmtree(self.key_dir) @classmethod def _call(cls, key_size, key_dir): from letsencrypt.crypto_util import init_save_key return init_save_key(key_size, key_dir, 'key-letsencrypt.pem') @mock.patch('letsencrypt.crypto_util.make_key') def test_success(self, mock_make): mock_make.return_value = 'key_pem' key = self._call(1024, self.key_dir) self.assertEqual(key.pem, 'key_pem') self.assertTrue('key-letsencrypt.pem' in key.file) @mock.patch('letsencrypt.crypto_util.make_key') def test_key_failure(self, mock_make): mock_make.side_effect = ValueError self.assertRaises(ValueError, self._call, 431, self.key_dir) class InitSaveCSRTest(unittest.TestCase): """Tests for letsencrypt.crypto_util.init_save_csr.""" def setUp(self): zope.component.provideUtility( mock.Mock(strict_permissions=True), interfaces.IConfig) self.csr_dir = tempfile.mkdtemp('csr_dir') def tearDown(self): shutil.rmtree(self.csr_dir) @mock.patch('letsencrypt.crypto_util.make_csr') @mock.patch('letsencrypt.crypto_util.le_util.make_or_verify_dir') def test_it(self, unused_mock_verify, mock_csr): from letsencrypt.crypto_util import init_save_csr mock_csr.return_value = ('csr_pem', 'csr_der') csr = init_save_csr( mock.Mock(pem='dummy_key'), 'example.com', self.csr_dir, 'csr-letsencrypt.pem') self.assertEqual(csr.data, 'csr_der') self.assertTrue('csr-letsencrypt.pem' in csr.file) class MakeCSRTest(unittest.TestCase): """Tests for letsencrypt.crypto_util.make_csr.""" @classmethod def _call(cls, *args, **kwargs): from letsencrypt.crypto_util import make_csr return make_csr(*args, **kwargs) def test_san(self): from letsencrypt.crypto_util import get_sans_from_csr # TODO: Fails for RSA256_KEY csr_pem, csr_der = self._call( RSA512_KEY, ['example.com', 'www.example.com']) self.assertEqual( ['example.com', 'www.example.com'], get_sans_from_csr(csr_pem)) self.assertEqual( ['example.com', 'www.example.com'], get_sans_from_csr( csr_der, OpenSSL.crypto.FILETYPE_ASN1)) class ValidCSRTest(unittest.TestCase): """Tests for letsencrypt.crypto_util.valid_csr.""" @classmethod def _call(cls, csr): from letsencrypt.crypto_util import valid_csr return valid_csr(csr) def test_valid_pem_true(self): self.assertTrue(self._call(test_util.load_vector('csr.pem'))) def test_valid_pem_san_true(self): self.assertTrue(self._call(test_util.load_vector('csr-san.pem'))) def test_valid_der_false(self): self.assertFalse(self._call(test_util.load_vector('csr.der'))) def test_valid_der_san_false(self): self.assertFalse(self._call(test_util.load_vector('csr-san.der'))) def test_empty_false(self): self.assertFalse(self._call('')) def test_random_false(self): self.assertFalse(self._call('foo bar')) class CSRMatchesPubkeyTest(unittest.TestCase): """Tests for letsencrypt.crypto_util.csr_matches_pubkey.""" @classmethod def _call(cls, *args, **kwargs): from letsencrypt.crypto_util import csr_matches_pubkey return csr_matches_pubkey(*args, **kwargs) def test_valid_true(self): self.assertTrue(self._call( test_util.load_vector('csr.pem'), RSA512_KEY)) def test_invalid_false(self): self.assertFalse(self._call( test_util.load_vector('csr.pem'), RSA256_KEY)) class MakeKeyTest(unittest.TestCase): # pylint: disable=too-few-public-methods """Tests for letsencrypt.crypto_util.make_key.""" def test_it(self): # pylint: disable=no-self-use from letsencrypt.crypto_util import make_key # Do not test larger keys as it takes too long. OpenSSL.crypto.load_privatekey( OpenSSL.crypto.FILETYPE_PEM, make_key(1024)) class ValidPrivkeyTest(unittest.TestCase): """Tests for letsencrypt.crypto_util.valid_privkey.""" @classmethod def _call(cls, privkey): from letsencrypt.crypto_util import valid_privkey return valid_privkey(privkey) def test_valid_true(self): self.assertTrue(self._call(RSA256_KEY)) def test_empty_false(self): self.assertFalse(self._call('')) def test_random_false(self): self.assertFalse(self._call('foo bar')) class GetSANsFromCertTest(unittest.TestCase): """Tests for letsencrypt.crypto_util.get_sans_from_cert.""" @classmethod def _call(cls, *args, **kwargs): from letsencrypt.crypto_util import get_sans_from_cert return get_sans_from_cert(*args, **kwargs) def test_single(self): self.assertEqual([], self._call(test_util.load_vector('cert.pem'))) def test_san(self): self.assertEqual( ['example.com', 'www.example.com'], self._call(test_util.load_vector('cert-san.pem'))) class GetSANsFromCSRTest(unittest.TestCase): """Tests for letsencrypt.crypto_util.get_sans_from_csr.""" @classmethod def _call(cls, *args, **kwargs): from letsencrypt.crypto_util import get_sans_from_csr return get_sans_from_csr(*args, **kwargs) def test_extract_one_san(self): self.assertEqual(['example.com'], self._call( test_util.load_vector('csr.pem'))) def test_extract_two_sans(self): self.assertEqual(['example.com', 'www.example.com'], self._call( test_util.load_vector('csr-san.pem'))) def test_extract_six_sans(self): self.assertEqual(self._call(test_util.load_vector('csr-6sans.pem')), ["example.com", "example.org", "example.net", "example.info", "subdomain.example.com", "other.subdomain.example.com"]) def test_parse_non_csr(self): self.assertRaises(OpenSSL.crypto.Error, self._call, "hello there") def test_parse_no_sans(self): self.assertEqual( [], self._call(test_util.load_vector('csr-nosans.pem'))) class CertLoaderTest(unittest.TestCase): """Tests for letsencrypt.crypto_util.pyopenssl_load_certificate""" def test_load_valid_cert(self): from letsencrypt.crypto_util import pyopenssl_load_certificate cert, file_type = pyopenssl_load_certificate(CERT) self.assertEqual(cert.digest('sha1'), OpenSSL.crypto.load_certificate(file_type, CERT).digest('sha1')) def test_load_invalid_cert(self): from letsencrypt.crypto_util import pyopenssl_load_certificate bad_cert_data = CERT.replace("BEGIN CERTIFICATE", "ASDFASDFASDF!!!") self.assertRaises( errors.Error, pyopenssl_load_certificate, bad_cert_data) class NotBeforeTest(unittest.TestCase): """Tests for letsencrypt.crypto_util.notBefore""" def test_notBefore(self): from letsencrypt.crypto_util import notBefore self.assertEqual(notBefore(CERT_PATH).isoformat(), '2014-12-11T22:34:45+00:00') class NotAfterTest(unittest.TestCase): """Tests for letsencrypt.crypto_util.notAfter""" def test_notAfter(self): from letsencrypt.crypto_util import notAfter self.assertEqual(notAfter(CERT_PATH).isoformat(), '2014-12-18T22:34:45+00:00') if __name__ == '__main__': unittest.main() # pragma: no cover letsencrypt-0.4.1/letsencrypt/tests/reporter_test.py0000644000175000017500000000564412665157707022506 0ustar bmwbmw00000000000000"""Tests for letsencrypt.reporter.""" import StringIO import sys import unittest class ReporterTest(unittest.TestCase): """Tests for letsencrypt.reporter.Reporter.""" def setUp(self): from letsencrypt import reporter self.reporter = reporter.Reporter() self.old_stdout = sys.stdout sys.stdout = StringIO.StringIO() def tearDown(self): sys.stdout = self.old_stdout def test_multiline_message(self): self.reporter.add_message("Line 1\nLine 2", self.reporter.LOW_PRIORITY) self.reporter.atexit_print_messages() output = sys.stdout.getvalue() self.assertTrue("Line 1\n" in output) self.assertTrue("Line 2" in output) def test_tty_print_empty(self): sys.stdout.isatty = lambda: True self.test_no_tty_print_empty() def test_no_tty_print_empty(self): self.reporter.print_messages() self.assertEqual(sys.stdout.getvalue(), "") try: raise ValueError except ValueError: self.reporter.print_messages() self.assertEqual(sys.stdout.getvalue(), "") def test_atexit_print_messages(self): self._add_messages() self.reporter.atexit_print_messages() output = sys.stdout.getvalue() self.assertTrue("IMPORTANT NOTES:" in output) self.assertTrue("High" in output) self.assertTrue("Med" in output) self.assertTrue("Low" in output) def test_tty_successful_exit(self): sys.stdout.isatty = lambda: True self._successful_exit_common() def test_no_tty_successful_exit(self): self._successful_exit_common() def test_tty_unsuccessful_exit(self): sys.stdout.isatty = lambda: True self._unsuccessful_exit_common() def test_no_tty_unsuccessful_exit(self): self._unsuccessful_exit_common() def _successful_exit_common(self): self._add_messages() self.reporter.print_messages() output = sys.stdout.getvalue() self.assertTrue("IMPORTANT NOTES:" in output) self.assertTrue("High" in output) self.assertTrue("Med" in output) self.assertTrue("Low" in output) def _unsuccessful_exit_common(self): self._add_messages() try: raise ValueError except ValueError: self.reporter.print_messages() output = sys.stdout.getvalue() self.assertTrue("IMPORTANT NOTES:" in output) self.assertTrue("High" in output) self.assertTrue("Med" not in output) self.assertTrue("Low" not in output) def _add_messages(self): self.reporter.add_message("High", self.reporter.HIGH_PRIORITY) self.reporter.add_message( "Med", self.reporter.MEDIUM_PRIORITY, on_crash=False) self.reporter.add_message( "Low", self.reporter.LOW_PRIORITY, on_crash=False) if __name__ == "__main__": unittest.main() # pragma: no cover letsencrypt-0.4.1/letsencrypt/tests/log_test.py0000644000175000017500000000325412665157707021420 0ustar bmwbmw00000000000000"""Tests for letsencrypt.log.""" import logging import unittest import mock class DialogHandlerTest(unittest.TestCase): def setUp(self): self.d = mock.MagicMock() from letsencrypt.log import DialogHandler self.handler = DialogHandler(height=2, width=6, d=self.d) self.handler.PADDING_HEIGHT = 2 self.handler.PADDING_WIDTH = 4 def test_adds_padding(self): self.handler.emit(logging.makeLogRecord({})) self.d.infobox.assert_called_once_with(mock.ANY, 4, 10) def test_args_in_msg_get_replaced(self): assert len('123456') <= self.handler.width self.handler.emit(logging.makeLogRecord( {'msg': '123%s', 'args': (456,)})) self.d.infobox.assert_called_once_with('123456', mock.ANY, mock.ANY) def test_wraps_nospace_is_greedy(self): assert len('1234567') > self.handler.width self.handler.emit(logging.makeLogRecord({'msg': '1234567'})) self.d.infobox.assert_called_once_with('123456\n7', mock.ANY, mock.ANY) def test_wraps_at_whitespace(self): assert len('123 567') > self.handler.width self.handler.emit(logging.makeLogRecord({'msg': '123 567'})) self.d.infobox.assert_called_once_with('123\n567', mock.ANY, mock.ANY) def test_only_last_lines_are_printed(self): assert len('a\nb\nc'.split()) > self.handler.height self.handler.emit(logging.makeLogRecord({'msg': 'a\n\nb\nc'})) self.d.infobox.assert_called_once_with('b\nc', mock.ANY, mock.ANY) def test_non_str(self): self.handler.emit(logging.makeLogRecord({'msg': {'foo': 'bar'}})) if __name__ == '__main__': unittest.main() # pragma: no cover letsencrypt-0.4.1/letsencrypt/tests/client_test.py0000644000175000017500000004600212665157707022113 0ustar bmwbmw00000000000000"""Tests for letsencrypt.client.""" import os import shutil import tempfile import unittest import OpenSSL import mock from acme import jose from letsencrypt import account from letsencrypt import errors from letsencrypt import le_util from letsencrypt.tests import test_util KEY = test_util.load_vector("rsa512_key.pem") CSR_SAN = test_util.load_vector("csr-san.der") class ConfigHelper(object): """Creates a dummy object to imitate a namespace object Example: cfg = ConfigHelper(redirect=True, hsts=False, uir=False) will result in: cfg.redirect=True, cfg.hsts=False, etc. """ def __init__(self, **kwds): self.__dict__.update(kwds) class RegisterTest(unittest.TestCase): """Tests for letsencrypt.client.register.""" def setUp(self): self.config = mock.MagicMock(rsa_key_size=1024, register_unsafely_without_email=False) self.account_storage = account.AccountMemoryStorage() self.tos_cb = mock.MagicMock() def _call(self): from letsencrypt.client import register return register(self.config, self.account_storage, self.tos_cb) def test_no_tos(self): with mock.patch("letsencrypt.client.acme_client.Client") as mock_client: mock_client.register().terms_of_service = "http://tos" with mock.patch("letsencrypt.account.report_new_account"): self.tos_cb.return_value = False self.assertRaises(errors.Error, self._call) self.tos_cb.return_value = True self._call() self.tos_cb = None self._call() def test_it(self): with mock.patch("letsencrypt.client.acme_client.Client"): with mock.patch("letsencrypt.account.report_new_account"): self._call() @mock.patch("letsencrypt.account.report_new_account") @mock.patch("letsencrypt.client.display_ops.get_email") def test_email_retry(self, _rep, mock_get_email): from acme import messages msg = "Validation of contact mailto:sousaphone@improbablylongggstring.tld failed" mx_err = messages.Error(detail=msg, typ="malformed", title="title") with mock.patch("letsencrypt.client.acme_client.Client") as mock_client: mock_client().register.side_effect = [mx_err, mock.MagicMock()] self._call() self.assertEqual(mock_get_email.call_count, 1) def test_needs_email(self): self.config.email = None self.assertRaises(errors.Error, self._call) @mock.patch("letsencrypt.client.logger") def test_without_email(self, mock_logger): with mock.patch("letsencrypt.client.acme_client.Client"): with mock.patch("letsencrypt.account.report_new_account"): self.config.email = None self.config.register_unsafely_without_email = True self._call() mock_logger.warn.assert_called_once_with(mock.ANY) def test_unsupported_error(self): from acme import messages msg = "Test" mx_err = messages.Error(detail=msg, typ="malformed", title="title") with mock.patch("letsencrypt.client.acme_client.Client") as mock_client: mock_client().register.side_effect = [mx_err, mock.MagicMock()] self.assertRaises(messages.Error, self._call) class ClientTest(unittest.TestCase): """Tests for letsencrypt.client.Client.""" def setUp(self): self.config = mock.MagicMock( no_verify_ssl=False, config_dir="/etc/letsencrypt") # pylint: disable=star-args self.account = mock.MagicMock(**{"key.pem": KEY}) self.eg_domains = ["example.com", "www.example.com"] from letsencrypt.client import Client with mock.patch("letsencrypt.client.acme_client.Client") as acme: self.acme_client = acme self.acme = acme.return_value = mock.MagicMock() self.client = Client( config=self.config, account_=self.account, dv_auth=None, installer=None) def test_init_acme_verify_ssl(self): net = self.acme_client.call_args[1]["net"] self.assertTrue(net.verify_ssl) def _mock_obtain_certificate(self): self.client.auth_handler = mock.MagicMock() self.acme.request_issuance.return_value = mock.sentinel.certr self.acme.fetch_chain.return_value = mock.sentinel.chain def _check_obtain_certificate(self): self.client.auth_handler.get_authorizations.assert_called_once_with(self.eg_domains) self.acme.request_issuance.assert_called_once_with( jose.ComparableX509(OpenSSL.crypto.load_certificate_request( OpenSSL.crypto.FILETYPE_ASN1, CSR_SAN)), self.client.auth_handler.get_authorizations()) self.acme.fetch_chain.assert_called_once_with(mock.sentinel.certr) # FIXME move parts of this to test_cli.py... @mock.patch("letsencrypt.client.logger") @mock.patch("letsencrypt.cli._process_domain") def test_obtain_certificate_from_csr(self, mock_process_domain, mock_logger): self._mock_obtain_certificate() from letsencrypt import cli test_csr = le_util.CSR(form="der", file=None, data=CSR_SAN) mock_parsed_args = mock.MagicMock() # The CLI should believe that this is a certonly request, because # a CSR would not be allowed with other kinds of requests! mock_parsed_args.verb = "certonly" with mock.patch("letsencrypt.client.le_util.CSR") as mock_CSR: mock_CSR.return_value = test_csr mock_parsed_args.domains = self.eg_domains[:] mock_parser = mock.MagicMock(cli.HelpfulArgumentParser) cli.HelpfulArgumentParser.handle_csr(mock_parser, mock_parsed_args) # make sure cli processing occurred cli_processed = (call[0][1] for call in mock_process_domain.call_args_list) self.assertEqual(set(cli_processed), set(("example.com", "www.example.com"))) # Now provoke an inconsistent domains error... mock_parsed_args.domains.append("hippopotamus.io") self.assertRaises(errors.ConfigurationError, cli.HelpfulArgumentParser.handle_csr, mock_parser, mock_parsed_args) self.assertEqual( (mock.sentinel.certr, mock.sentinel.chain), self.client.obtain_certificate_from_csr(self.eg_domains, test_csr)) # and that the cert was obtained correctly self._check_obtain_certificate() # Test for no auth_handler self.client.auth_handler = None self.assertRaises( errors.Error, self.client.obtain_certificate_from_csr, self.eg_domains, test_csr) mock_logger.warning.assert_called_once_with(mock.ANY) @mock.patch("letsencrypt.client.crypto_util") def test_obtain_certificate(self, mock_crypto_util): self._mock_obtain_certificate() csr = le_util.CSR(form="der", file=None, data=CSR_SAN) mock_crypto_util.init_save_csr.return_value = csr mock_crypto_util.init_save_key.return_value = mock.sentinel.key domains = ["example.com", "www.example.com"] self.assertEqual( self.client.obtain_certificate(domains), (mock.sentinel.certr, mock.sentinel.chain, mock.sentinel.key, csr)) mock_crypto_util.init_save_key.assert_called_once_with( self.config.rsa_key_size, self.config.key_dir) mock_crypto_util.init_save_csr.assert_called_once_with( mock.sentinel.key, domains, self.config.csr_dir) self._check_obtain_certificate() def test_save_certificate(self): certs = ["matching_cert.pem", "cert.pem", "cert-san.pem"] tmp_path = tempfile.mkdtemp() os.chmod(tmp_path, 0o755) # TODO: really?? certr = mock.MagicMock(body=test_util.load_comparable_cert(certs[0])) chain_cert = [test_util.load_comparable_cert(certs[1]), test_util.load_comparable_cert(certs[2])] candidate_cert_path = os.path.join(tmp_path, "certs", "cert.pem") candidate_chain_path = os.path.join(tmp_path, "chains", "chain.pem") candidate_fullchain_path = os.path.join(tmp_path, "chains", "fullchain.pem") cert_path, chain_path, fullchain_path = self.client.save_certificate( certr, chain_cert, candidate_cert_path, candidate_chain_path, candidate_fullchain_path) self.assertEqual(os.path.dirname(cert_path), os.path.dirname(candidate_cert_path)) self.assertEqual(os.path.dirname(chain_path), os.path.dirname(candidate_chain_path)) self.assertEqual(os.path.dirname(fullchain_path), os.path.dirname(candidate_fullchain_path)) with open(cert_path, "r") as cert_file: cert_contents = cert_file.read() self.assertEqual(cert_contents, test_util.load_vector(certs[0])) with open(chain_path, "r") as chain_file: chain_contents = chain_file.read() self.assertEqual(chain_contents, test_util.load_vector(certs[1]) + test_util.load_vector(certs[2])) shutil.rmtree(tmp_path) def test_deploy_certificate_success(self): self.assertRaises(errors.Error, self.client.deploy_certificate, ["foo.bar"], "key", "cert", "chain", "fullchain") installer = mock.MagicMock() self.client.installer = installer self.client.deploy_certificate( ["foo.bar"], "key", "cert", "chain", "fullchain") installer.deploy_cert.assert_called_once_with( cert_path=os.path.abspath("cert"), chain_path=os.path.abspath("chain"), domain='foo.bar', fullchain_path='fullchain', key_path=os.path.abspath("key")) self.assertEqual(installer.save.call_count, 2) installer.restart.assert_called_once_with() def test_deploy_certificate_failure(self): installer = mock.MagicMock() self.client.installer = installer installer.deploy_cert.side_effect = errors.PluginError self.assertRaises(errors.PluginError, self.client.deploy_certificate, ["foo.bar"], "key", "cert", "chain", "fullchain") installer.recovery_routine.assert_called_once_with() def test_deploy_certificate_save_failure(self): installer = mock.MagicMock() self.client.installer = installer installer.save.side_effect = errors.PluginError self.assertRaises(errors.PluginError, self.client.deploy_certificate, ["foo.bar"], "key", "cert", "chain", "fullchain") installer.recovery_routine.assert_called_once_with() @mock.patch("letsencrypt.client.zope.component.getUtility") def test_deploy_certificate_restart_failure(self, mock_get_utility): installer = mock.MagicMock() installer.restart.side_effect = [errors.PluginError, None] self.client.installer = installer self.assertRaises(errors.PluginError, self.client.deploy_certificate, ["foo.bar"], "key", "cert", "chain", "fullchain") self.assertEqual(mock_get_utility().add_message.call_count, 1) installer.rollback_checkpoints.assert_called_once_with() self.assertEqual(installer.restart.call_count, 2) @mock.patch("letsencrypt.client.zope.component.getUtility") def test_deploy_certificate_restart_failure2(self, mock_get_utility): installer = mock.MagicMock() installer.restart.side_effect = errors.PluginError installer.rollback_checkpoints.side_effect = errors.ReverterError self.client.installer = installer self.assertRaises(errors.PluginError, self.client.deploy_certificate, ["foo.bar"], "key", "cert", "chain", "fullchain") self.assertEqual(mock_get_utility().add_message.call_count, 1) installer.rollback_checkpoints.assert_called_once_with() self.assertEqual(installer.restart.call_count, 1) @mock.patch("letsencrypt.client.enhancements") def test_enhance_config(self, mock_enhancements): config = ConfigHelper(redirect=True, hsts=False, uir=False) self.assertRaises(errors.Error, self.client.enhance_config, ["foo.bar"], config) mock_enhancements.ask.return_value = True installer = mock.MagicMock() self.client.installer = installer installer.supported_enhancements.return_value = ["redirect"] self.client.enhance_config(["foo.bar"], config) installer.enhance.assert_called_once_with("foo.bar", "redirect", None) self.assertEqual(installer.save.call_count, 1) installer.restart.assert_called_once_with() @mock.patch("letsencrypt.client.enhancements") def test_enhance_config_no_ask(self, mock_enhancements): config = ConfigHelper(redirect=True, hsts=False, uir=False) self.assertRaises(errors.Error, self.client.enhance_config, ["foo.bar"], config) mock_enhancements.ask.return_value = True installer = mock.MagicMock() self.client.installer = installer installer.supported_enhancements.return_value = ["redirect", "ensure-http-header"] config = ConfigHelper(redirect=True, hsts=False, uir=False) self.client.enhance_config(["foo.bar"], config) installer.enhance.assert_called_with("foo.bar", "redirect", None) config = ConfigHelper(redirect=False, hsts=True, uir=False) self.client.enhance_config(["foo.bar"], config) installer.enhance.assert_called_with("foo.bar", "ensure-http-header", "Strict-Transport-Security") config = ConfigHelper(redirect=False, hsts=False, uir=True) self.client.enhance_config(["foo.bar"], config) installer.enhance.assert_called_with("foo.bar", "ensure-http-header", "Upgrade-Insecure-Requests") self.assertEqual(installer.save.call_count, 3) self.assertEqual(installer.restart.call_count, 3) @mock.patch("letsencrypt.client.enhancements") def test_enhance_config_unsupported(self, mock_enhancements): installer = mock.MagicMock() self.client.installer = installer installer.supported_enhancements.return_value = [] config = ConfigHelper(redirect=None, hsts=True, uir=True) self.client.enhance_config(["foo.bar"], config) installer.enhance.assert_not_called() mock_enhancements.ask.assert_not_called() def test_enhance_config_no_installer(self): config = ConfigHelper(redirect=True, hsts=False, uir=False) self.assertRaises(errors.Error, self.client.enhance_config, ["foo.bar"], config) @mock.patch("letsencrypt.client.zope.component.getUtility") @mock.patch("letsencrypt.client.enhancements") def test_enhance_config_enhance_failure(self, mock_enhancements, mock_get_utility): mock_enhancements.ask.return_value = True installer = mock.MagicMock() self.client.installer = installer installer.supported_enhancements.return_value = ["redirect"] installer.enhance.side_effect = errors.PluginError config = ConfigHelper(redirect=True, hsts=False, uir=False) self.assertRaises(errors.PluginError, self.client.enhance_config, ["foo.bar"], config) installer.recovery_routine.assert_called_once_with() self.assertEqual(mock_get_utility().add_message.call_count, 1) @mock.patch("letsencrypt.client.zope.component.getUtility") @mock.patch("letsencrypt.client.enhancements") def test_enhance_config_save_failure(self, mock_enhancements, mock_get_utility): mock_enhancements.ask.return_value = True installer = mock.MagicMock() self.client.installer = installer installer.supported_enhancements.return_value = ["redirect"] installer.save.side_effect = errors.PluginError config = ConfigHelper(redirect=True, hsts=False, uir=False) self.assertRaises(errors.PluginError, self.client.enhance_config, ["foo.bar"], config) installer.recovery_routine.assert_called_once_with() self.assertEqual(mock_get_utility().add_message.call_count, 1) @mock.patch("letsencrypt.client.zope.component.getUtility") @mock.patch("letsencrypt.client.enhancements") def test_enhance_config_restart_failure(self, mock_enhancements, mock_get_utility): mock_enhancements.ask.return_value = True installer = mock.MagicMock() self.client.installer = installer installer.supported_enhancements.return_value = ["redirect"] installer.restart.side_effect = [errors.PluginError, None] config = ConfigHelper(redirect=True, hsts=False, uir=False) self.assertRaises(errors.PluginError, self.client.enhance_config, ["foo.bar"], config) self.assertEqual(mock_get_utility().add_message.call_count, 1) installer.rollback_checkpoints.assert_called_once_with() self.assertEqual(installer.restart.call_count, 2) @mock.patch("letsencrypt.client.zope.component.getUtility") @mock.patch("letsencrypt.client.enhancements") def test_enhance_config_restart_failure2(self, mock_enhancements, mock_get_utility): mock_enhancements.ask.return_value = True installer = mock.MagicMock() self.client.installer = installer installer.supported_enhancements.return_value = ["redirect"] installer.restart.side_effect = errors.PluginError installer.rollback_checkpoints.side_effect = errors.ReverterError config = ConfigHelper(redirect=True, hsts=False, uir=False) self.assertRaises(errors.PluginError, self.client.enhance_config, ["foo.bar"], config) self.assertEqual(mock_get_utility().add_message.call_count, 1) installer.rollback_checkpoints.assert_called_once_with() self.assertEqual(installer.restart.call_count, 1) class RollbackTest(unittest.TestCase): """Tests for letsencrypt.client.rollback.""" def setUp(self): self.m_install = mock.MagicMock() @classmethod def _call(cls, checkpoints, side_effect): from letsencrypt.client import rollback with mock.patch("letsencrypt.client" ".display_ops.pick_installer") as mock_pick_installer: mock_pick_installer.side_effect = side_effect rollback(None, checkpoints, {}, mock.MagicMock()) def test_no_problems(self): self._call(1, self.m_install) self.assertEqual(self.m_install().rollback_checkpoints.call_count, 1) self.assertEqual(self.m_install().restart.call_count, 1) def test_no_installer(self): self._call(1, None) # Just make sure no exceptions are raised if __name__ == "__main__": unittest.main() # pragma: no cover letsencrypt-0.4.1/letsencrypt/tests/le_util_test.py0000644000175000017500000002446012665157707022276 0ustar bmwbmw00000000000000"""Tests for letsencrypt.le_util.""" import argparse import errno import os import shutil import stat import StringIO import tempfile import unittest import mock from letsencrypt import errors class RunScriptTest(unittest.TestCase): """Tests for letsencrypt.le_util.run_script.""" @classmethod def _call(cls, params): from letsencrypt.le_util import run_script return run_script(params) @mock.patch("letsencrypt.le_util.subprocess.Popen") def test_default(self, mock_popen): """These will be changed soon enough with reload.""" mock_popen().returncode = 0 mock_popen().communicate.return_value = ("stdout", "stderr") out, err = self._call(["test"]) self.assertEqual(out, "stdout") self.assertEqual(err, "stderr") @mock.patch("letsencrypt.le_util.subprocess.Popen") def test_bad_process(self, mock_popen): mock_popen.side_effect = OSError self.assertRaises(errors.SubprocessError, self._call, ["test"]) @mock.patch("letsencrypt.le_util.subprocess.Popen") def test_failure(self, mock_popen): mock_popen().communicate.return_value = ("", "") mock_popen().returncode = 1 self.assertRaises(errors.SubprocessError, self._call, ["test"]) class ExeExistsTest(unittest.TestCase): """Tests for letsencrypt.le_util.exe_exists.""" @classmethod def _call(cls, exe): from letsencrypt.le_util import exe_exists return exe_exists(exe) @mock.patch("letsencrypt.le_util.os.path.isfile") @mock.patch("letsencrypt.le_util.os.access") def test_full_path(self, mock_access, mock_isfile): mock_access.return_value = True mock_isfile.return_value = True self.assertTrue(self._call("/path/to/exe")) @mock.patch("letsencrypt.le_util.os.path.isfile") @mock.patch("letsencrypt.le_util.os.access") def test_on_path(self, mock_access, mock_isfile): mock_access.return_value = True mock_isfile.return_value = True self.assertTrue(self._call("exe")) @mock.patch("letsencrypt.le_util.os.path.isfile") @mock.patch("letsencrypt.le_util.os.access") def test_not_found(self, mock_access, mock_isfile): mock_access.return_value = False mock_isfile.return_value = True self.assertFalse(self._call("exe")) class MakeOrVerifyDirTest(unittest.TestCase): """Tests for letsencrypt.le_util.make_or_verify_dir. Note that it is not possible to test for a wrong directory owner, as this testing script would have to be run as root. """ def setUp(self): self.root_path = tempfile.mkdtemp() self.path = os.path.join(self.root_path, "foo") os.mkdir(self.path, 0o400) self.uid = os.getuid() def tearDown(self): shutil.rmtree(self.root_path, ignore_errors=True) def _call(self, directory, mode): from letsencrypt.le_util import make_or_verify_dir return make_or_verify_dir(directory, mode, self.uid, strict=True) def test_creates_dir_when_missing(self): path = os.path.join(self.root_path, "bar") self._call(path, 0o650) self.assertTrue(os.path.isdir(path)) self.assertEqual(stat.S_IMODE(os.stat(path).st_mode), 0o650) def test_existing_correct_mode_does_not_fail(self): self._call(self.path, 0o400) self.assertEqual(stat.S_IMODE(os.stat(self.path).st_mode), 0o400) def test_existing_wrong_mode_fails(self): self.assertRaises(errors.Error, self._call, self.path, 0o600) def test_reraises_os_error(self): with mock.patch.object(os, "makedirs") as makedirs: makedirs.side_effect = OSError() self.assertRaises(OSError, self._call, "bar", 12312312) class CheckPermissionsTest(unittest.TestCase): """Tests for letsencrypt.le_util.check_permissions. Note that it is not possible to test for a wrong file owner, as this testing script would have to be run as root. """ def setUp(self): _, self.path = tempfile.mkstemp() self.uid = os.getuid() def tearDown(self): os.remove(self.path) def _call(self, mode): from letsencrypt.le_util import check_permissions return check_permissions(self.path, mode, self.uid) def test_ok_mode(self): os.chmod(self.path, 0o600) self.assertTrue(self._call(0o600)) def test_wrong_mode(self): os.chmod(self.path, 0o400) self.assertFalse(self._call(0o600)) class UniqueFileTest(unittest.TestCase): """Tests for letsencrypt.le_util.unique_file.""" def setUp(self): self.root_path = tempfile.mkdtemp() self.default_name = os.path.join(self.root_path, "foo.txt") def tearDown(self): shutil.rmtree(self.root_path, ignore_errors=True) def _call(self, mode=0o600): from letsencrypt.le_util import unique_file return unique_file(self.default_name, mode) def test_returns_fd_for_writing(self): fd, name = self._call() fd.write("bar") fd.close() self.assertEqual(open(name).read(), "bar") def test_right_mode(self): self.assertEqual(0o700, os.stat(self._call(0o700)[1]).st_mode & 0o777) self.assertEqual(0o100, os.stat(self._call(0o100)[1]).st_mode & 0o777) def test_default_exists(self): name1 = self._call()[1] # create 0000_foo.txt name2 = self._call()[1] name3 = self._call()[1] self.assertNotEqual(name1, name2) self.assertNotEqual(name1, name3) self.assertNotEqual(name2, name3) self.assertEqual(os.path.dirname(name1), self.root_path) self.assertEqual(os.path.dirname(name2), self.root_path) self.assertEqual(os.path.dirname(name3), self.root_path) basename1 = os.path.basename(name2) self.assertTrue(basename1.endswith("foo.txt")) basename2 = os.path.basename(name2) self.assertTrue(basename2.endswith("foo.txt")) basename3 = os.path.basename(name3) self.assertTrue(basename3.endswith("foo.txt")) class UniqueLineageNameTest(unittest.TestCase): """Tests for letsencrypt.le_util.unique_lineage_name.""" def setUp(self): self.root_path = tempfile.mkdtemp() def tearDown(self): shutil.rmtree(self.root_path, ignore_errors=True) def _call(self, filename, mode=0o777): from letsencrypt.le_util import unique_lineage_name return unique_lineage_name(self.root_path, filename, mode) def test_basic(self): f, path = self._call("wow") self.assertTrue(isinstance(f, file)) self.assertEqual(os.path.join(self.root_path, "wow.conf"), path) def test_multiple(self): for _ in xrange(10): f, name = self._call("wow") self.assertTrue(isinstance(f, file)) self.assertTrue(isinstance(name, str)) self.assertTrue("wow-0009.conf" in name) @mock.patch("letsencrypt.le_util.os.fdopen") def test_failure(self, mock_fdopen): err = OSError("whoops") err.errno = errno.EIO mock_fdopen.side_effect = err self.assertRaises(OSError, self._call, "wow") @mock.patch("letsencrypt.le_util.os.fdopen") def test_subsequent_failure(self, mock_fdopen): self._call("wow") err = OSError("whoops") err.errno = errno.EIO mock_fdopen.side_effect = err self.assertRaises(OSError, self._call, "wow") class SafelyRemoveTest(unittest.TestCase): """Tests for letsencrypt.le_util.safely_remove.""" def setUp(self): self.tmp = tempfile.mkdtemp() self.path = os.path.join(self.tmp, "foo") def tearDown(self): shutil.rmtree(self.tmp) def _call(self): from letsencrypt.le_util import safely_remove return safely_remove(self.path) def test_exists(self): with open(self.path, "w"): pass # just create the file self._call() self.assertFalse(os.path.exists(self.path)) def test_missing(self): self._call() # no error, yay! self.assertFalse(os.path.exists(self.path)) @mock.patch("letsencrypt.le_util.os.remove") def test_other_error_passthrough(self, mock_remove): mock_remove.side_effect = OSError self.assertRaises(OSError, self._call) class SafeEmailTest(unittest.TestCase): """Test safe_email.""" @classmethod def _call(cls, addr): from letsencrypt.le_util import safe_email return safe_email(addr) def test_valid_emails(self): addrs = [ "letsencrypt@letsencrypt.org", "tbd.ade@gmail.com", "abc_def.jdk@hotmail.museum", ] for addr in addrs: self.assertTrue(self._call(addr), "%s failed." % addr) def test_invalid_emails(self): addrs = [ "letsencrypt@letsencrypt..org", ".tbd.ade@gmail.com", "~/abc_def.jdk@hotmail.museum", ] for addr in addrs: self.assertFalse(self._call(addr), "%s failed." % addr) class AddDeprecatedArgumentTest(unittest.TestCase): """Test add_deprecated_argument.""" def setUp(self): self.parser = argparse.ArgumentParser() def _call(self, argument_name, nargs): from letsencrypt.le_util import add_deprecated_argument add_deprecated_argument(self.parser.add_argument, argument_name, nargs) def test_warning_no_arg(self): self._call("--old-option", 0) stderr = self._get_argparse_warnings(["--old-option"]) self.assertTrue("--old-option is deprecated" in stderr) def test_warning_with_arg(self): self._call("--old-option", 1) stderr = self._get_argparse_warnings(["--old-option", "42"]) self.assertTrue("--old-option is deprecated" in stderr) def _get_argparse_warnings(self, args): stderr = StringIO.StringIO() with mock.patch("letsencrypt.le_util.sys.stderr", new=stderr): self.parser.parse_args(args) return stderr.getvalue() def test_help(self): self._call("--old-option", 2) stdout = StringIO.StringIO() with mock.patch("letsencrypt.le_util.sys.stdout", new=stdout): try: self.parser.parse_args(["-h"]) except SystemExit: pass self.assertTrue("--old-option" not in stdout.getvalue()) if __name__ == "__main__": unittest.main() # pragma: no cover letsencrypt-0.4.1/letsencrypt/tests/auth_handler_test.py0000644000175000017500000004444112665157707023300 0ustar bmwbmw00000000000000"""Tests for letsencrypt.auth_handler.""" import functools import logging import unittest import mock from acme import challenges from acme import client as acme_client from acme import messages from letsencrypt import achallenges from letsencrypt import errors from letsencrypt import le_util from letsencrypt.tests import acme_util class ChallengeFactoryTest(unittest.TestCase): # pylint: disable=protected-access def setUp(self): from letsencrypt.auth_handler import AuthHandler # Account is mocked... self.handler = AuthHandler( None, None, None, mock.Mock(key="mock_key")) self.dom = "test" self.handler.authzr[self.dom] = acme_util.gen_authzr( messages.STATUS_PENDING, self.dom, acme_util.CHALLENGES, [messages.STATUS_PENDING] * 6, False) def test_all(self): cont_c, dv_c = self.handler._challenge_factory( self.dom, range(0, len(acme_util.CHALLENGES))) self.assertEqual( [achall.chall for achall in cont_c], acme_util.CONT_CHALLENGES) self.assertEqual( [achall.chall for achall in dv_c], acme_util.DV_CHALLENGES) def test_one_dv_one_cont(self): cont_c, dv_c = self.handler._challenge_factory(self.dom, [1, 3]) self.assertEqual( [achall.chall for achall in cont_c], [acme_util.RECOVERY_CONTACT]) self.assertEqual([achall.chall for achall in dv_c], [acme_util.TLSSNI01]) def test_unrecognized(self): self.handler.authzr["failure.com"] = acme_util.gen_authzr( messages.STATUS_PENDING, "failure.com", [mock.Mock(chall="chall", typ="unrecognized")], [messages.STATUS_PENDING]) self.assertRaises( errors.Error, self.handler._challenge_factory, "failure.com", [0]) class GetAuthorizationsTest(unittest.TestCase): """get_authorizations test. This tests everything except for all functions under _poll_challenges. """ def setUp(self): from letsencrypt.auth_handler import AuthHandler self.mock_dv_auth = mock.MagicMock(name="ApacheConfigurator") self.mock_cont_auth = mock.MagicMock(name="ContinuityAuthenticator") self.mock_dv_auth.get_chall_pref.return_value = [challenges.TLSSNI01] self.mock_cont_auth.get_chall_pref.return_value = [ challenges.RecoveryContact] self.mock_cont_auth.perform.side_effect = gen_auth_resp self.mock_dv_auth.perform.side_effect = gen_auth_resp self.mock_account = mock.Mock(key=le_util.Key("file_path", "PEM")) self.mock_net = mock.MagicMock(spec=acme_client.Client) self.handler = AuthHandler( self.mock_dv_auth, self.mock_cont_auth, self.mock_net, self.mock_account) logging.disable(logging.CRITICAL) def tearDown(self): logging.disable(logging.NOTSET) @mock.patch("letsencrypt.auth_handler.AuthHandler._poll_challenges") def test_name1_tls_sni_01_1(self, mock_poll): self.mock_net.request_domain_challenges.side_effect = functools.partial( gen_dom_authzr, challs=acme_util.DV_CHALLENGES) mock_poll.side_effect = self._validate_all authzr = self.handler.get_authorizations(["0"]) self.assertEqual(self.mock_net.answer_challenge.call_count, 1) self.assertEqual(mock_poll.call_count, 1) chall_update = mock_poll.call_args[0][0] self.assertEqual(chall_update.keys(), ["0"]) self.assertEqual(len(chall_update.values()), 1) self.assertEqual(self.mock_dv_auth.cleanup.call_count, 1) self.assertEqual(self.mock_cont_auth.cleanup.call_count, 0) # Test if list first element is TLSSNI01, use typ because it is an achall self.assertEqual( self.mock_dv_auth.cleanup.call_args[0][0][0].typ, "tls-sni-01") self.assertEqual(len(authzr), 1) @mock.patch("letsencrypt.auth_handler.AuthHandler._poll_challenges") def test_name3_tls_sni_01_3_rectok_3(self, mock_poll): self.mock_net.request_domain_challenges.side_effect = functools.partial( gen_dom_authzr, challs=acme_util.CHALLENGES) mock_poll.side_effect = self._validate_all authzr = self.handler.get_authorizations(["0", "1", "2"]) self.assertEqual(self.mock_net.answer_challenge.call_count, 6) # Check poll call self.assertEqual(mock_poll.call_count, 1) chall_update = mock_poll.call_args[0][0] self.assertEqual(len(chall_update.keys()), 3) self.assertTrue("0" in chall_update.keys()) self.assertEqual(len(chall_update["0"]), 2) self.assertTrue("1" in chall_update.keys()) self.assertEqual(len(chall_update["1"]), 2) self.assertTrue("2" in chall_update.keys()) self.assertEqual(len(chall_update["2"]), 2) self.assertEqual(self.mock_dv_auth.cleanup.call_count, 1) self.assertEqual(self.mock_cont_auth.cleanup.call_count, 1) self.assertEqual(len(authzr), 3) def test_perform_failure(self): self.mock_net.request_domain_challenges.side_effect = functools.partial( gen_dom_authzr, challs=acme_util.CHALLENGES) self.mock_dv_auth.perform.side_effect = errors.AuthorizationError self.assertRaises( errors.AuthorizationError, self.handler.get_authorizations, ["0"]) def _validate_all(self, unused_1, unused_2): for dom in self.handler.authzr.keys(): azr = self.handler.authzr[dom] self.handler.authzr[dom] = acme_util.gen_authzr( messages.STATUS_VALID, dom, [challb.chall for challb in azr.body.challenges], [messages.STATUS_VALID] * len(azr.body.challenges), azr.body.combinations) class PollChallengesTest(unittest.TestCase): # pylint: disable=protected-access """Test poll challenges.""" def setUp(self): from letsencrypt.auth_handler import challb_to_achall from letsencrypt.auth_handler import AuthHandler # Account and network are mocked... self.mock_net = mock.MagicMock() self.handler = AuthHandler( None, None, self.mock_net, mock.Mock(key="mock_key")) self.doms = ["0", "1", "2"] self.handler.authzr[self.doms[0]] = acme_util.gen_authzr( messages.STATUS_PENDING, self.doms[0], acme_util.DV_CHALLENGES, [messages.STATUS_PENDING] * 3, False) self.handler.authzr[self.doms[1]] = acme_util.gen_authzr( messages.STATUS_PENDING, self.doms[1], acme_util.DV_CHALLENGES, [messages.STATUS_PENDING] * 3, False) self.handler.authzr[self.doms[2]] = acme_util.gen_authzr( messages.STATUS_PENDING, self.doms[2], acme_util.DV_CHALLENGES, [messages.STATUS_PENDING] * 3, False) self.chall_update = {} for dom in self.doms: self.chall_update[dom] = [ challb_to_achall(challb, mock.Mock(key="dummy_key"), dom) for challb in self.handler.authzr[dom].body.challenges] @mock.patch("letsencrypt.auth_handler.time") def test_poll_challenges(self, unused_mock_time): self.mock_net.poll.side_effect = self._mock_poll_solve_one_valid self.handler._poll_challenges(self.chall_update, False) for authzr in self.handler.authzr.values(): self.assertEqual(authzr.body.status, messages.STATUS_VALID) @mock.patch("letsencrypt.auth_handler.time") def test_poll_challenges_failure_best_effort(self, unused_mock_time): self.mock_net.poll.side_effect = self._mock_poll_solve_one_invalid self.handler._poll_challenges(self.chall_update, True) for authzr in self.handler.authzr.values(): self.assertEqual(authzr.body.status, messages.STATUS_PENDING) @mock.patch("letsencrypt.auth_handler.time") @mock.patch("letsencrypt.auth_handler.zope.component.getUtility") def test_poll_challenges_failure(self, unused_mock_time, unused_mock_zope): self.mock_net.poll.side_effect = self._mock_poll_solve_one_invalid self.assertRaises( errors.AuthorizationError, self.handler._poll_challenges, self.chall_update, False) @mock.patch("letsencrypt.auth_handler.time") def test_unable_to_find_challenge_status(self, unused_mock_time): from letsencrypt.auth_handler import challb_to_achall self.mock_net.poll.side_effect = self._mock_poll_solve_one_valid self.chall_update[self.doms[0]].append( challb_to_achall(acme_util.RECOVERY_CONTACT_P, "key", self.doms[0])) self.assertRaises( errors.AuthorizationError, self.handler._poll_challenges, self.chall_update, False) def test_verify_authzr_failure(self): self.assertRaises( errors.AuthorizationError, self.handler.verify_authzr_complete) def _mock_poll_solve_one_valid(self, authzr): # Pending here because my dummy script won't change the full status. # Basically it didn't raise an error and it stopped earlier than # Making all challenges invalid which would make mock_poll_solve_one # change authzr to invalid return self._mock_poll_solve_one_chall(authzr, messages.STATUS_VALID) def _mock_poll_solve_one_invalid(self, authzr): return self._mock_poll_solve_one_chall(authzr, messages.STATUS_INVALID) def _mock_poll_solve_one_chall(self, authzr, desired_status): # pylint: disable=no-self-use """Dummy method that solves one chall at a time to desired_status. When all are solved.. it changes authzr.status to desired_status """ new_challbs = authzr.body.challenges for challb in authzr.body.challenges: if challb.status != desired_status: new_challbs = tuple( challb_temp if challb_temp != challb else acme_util.chall_to_challb(challb.chall, desired_status) for challb_temp in authzr.body.challenges ) break if all(test_challb.status == desired_status for test_challb in new_challbs): status_ = desired_status else: status_ = authzr.body.status new_authzr = messages.AuthorizationResource( uri=authzr.uri, new_cert_uri=authzr.new_cert_uri, body=messages.Authorization( identifier=authzr.body.identifier, challenges=new_challbs, combinations=authzr.body.combinations, status=status_, ), ) return (new_authzr, "response") class ChallbToAchallTest(unittest.TestCase): """Tests for letsencrypt.auth_handler.challb_to_achall.""" def _call(self, challb): from letsencrypt.auth_handler import challb_to_achall return challb_to_achall(challb, "account_key", "domain") def test_it(self): self.assertEqual( self._call(acme_util.HTTP01_P), achallenges.KeyAuthorizationAnnotatedChallenge( challb=acme_util.HTTP01_P, account_key="account_key", domain="domain"), ) class GenChallengePathTest(unittest.TestCase): """Tests for letsencrypt.auth_handler.gen_challenge_path. .. todo:: Add more tests for dumb_path... depending on what we want to do. """ def setUp(self): logging.disable(logging.fatal) def tearDown(self): logging.disable(logging.NOTSET) @classmethod def _call(cls, challbs, preferences, combinations): from letsencrypt.auth_handler import gen_challenge_path return gen_challenge_path(challbs, preferences, combinations) def test_common_case(self): """Given TLSSNI01 and HTTP01 with appropriate combos.""" challbs = (acme_util.TLSSNI01_P, acme_util.HTTP01_P) prefs = [challenges.TLSSNI01] combos = ((0,), (1,)) # Smart then trivial dumb path test self.assertEqual(self._call(challbs, prefs, combos), (0,)) self.assertTrue(self._call(challbs, prefs, None)) # Rearrange order... self.assertEqual(self._call(challbs[::-1], prefs, combos), (1,)) self.assertTrue(self._call(challbs[::-1], prefs, None)) def test_common_case_with_continuity(self): challbs = (acme_util.POP_P, acme_util.RECOVERY_CONTACT_P, acme_util.TLSSNI01_P, acme_util.HTTP01_P) prefs = [challenges.ProofOfPossession, challenges.TLSSNI01] combos = acme_util.gen_combos(challbs) self.assertEqual(self._call(challbs, prefs, combos), (0, 2)) # dumb_path() trivial test self.assertTrue(self._call(challbs, prefs, None)) def test_full_cont_server(self): challbs = (acme_util.RECOVERY_CONTACT_P, acme_util.POP_P, acme_util.TLSSNI01_P, acme_util.HTTP01_P, acme_util.DNS_P) # Typical webserver client that can do everything except DNS # Attempted to make the order realistic prefs = [challenges.ProofOfPossession, challenges.HTTP01, challenges.TLSSNI01, challenges.RecoveryContact] combos = acme_util.gen_combos(challbs) self.assertEqual(self._call(challbs, prefs, combos), (1, 3)) # Dumb path trivial test self.assertTrue(self._call(challbs, prefs, None)) def test_not_supported(self): challbs = (acme_util.POP_P, acme_util.TLSSNI01_P) prefs = [challenges.TLSSNI01] combos = ((0, 1),) self.assertRaises( errors.AuthorizationError, self._call, challbs, prefs, combos) class MutuallyExclusiveTest(unittest.TestCase): """Tests for letsencrypt.auth_handler.mutually_exclusive.""" # pylint: disable=missing-docstring,too-few-public-methods class A(object): pass class B(object): pass class C(object): pass class D(C): pass @classmethod def _call(cls, chall1, chall2, different=False): from letsencrypt.auth_handler import mutually_exclusive return mutually_exclusive(chall1, chall2, groups=frozenset([ frozenset([cls.A, cls.B]), frozenset([cls.A, cls.C]), ]), different=different) def test_group_members(self): self.assertFalse(self._call(self.A(), self.B())) self.assertFalse(self._call(self.A(), self.C())) def test_cross_group(self): self.assertTrue(self._call(self.B(), self.C())) def test_same_type(self): self.assertFalse(self._call(self.A(), self.A(), different=False)) self.assertTrue(self._call(self.A(), self.A(), different=True)) # in particular... obj = self.A() self.assertFalse(self._call(obj, obj, different=False)) self.assertTrue(self._call(obj, obj, different=True)) def test_subclass(self): self.assertFalse(self._call(self.A(), self.D())) self.assertFalse(self._call(self.D(), self.A())) class IsPreferredTest(unittest.TestCase): """Tests for letsencrypt.auth_handler.is_preferred.""" @classmethod def _call(cls, chall, satisfied): from letsencrypt.auth_handler import is_preferred return is_preferred(chall, satisfied, exclusive_groups=frozenset([ frozenset([challenges.TLSSNI01, challenges.HTTP01]), frozenset([challenges.DNS, challenges.HTTP01]), ])) def test_empty_satisfied(self): self.assertTrue(self._call(acme_util.DNS_P, frozenset())) def test_mutually_exclusvie(self): self.assertFalse( self._call( acme_util.TLSSNI01_P, frozenset([acme_util.HTTP01_P]))) def test_mutually_exclusive_same_type(self): self.assertTrue( self._call(acme_util.TLSSNI01_P, frozenset([acme_util.TLSSNI01_P]))) class ReportFailedChallsTest(unittest.TestCase): """Tests for letsencrypt.auth_handler._report_failed_challs.""" # pylint: disable=protected-access def setUp(self): kwargs = { "chall": acme_util.HTTP01, "uri": "uri", "status": messages.STATUS_INVALID, "error": messages.Error(typ="urn:acme:error:tls", detail="detail"), } # Prevent future regressions if the error type changes self.assertTrue(kwargs["error"].description is not None) self.http01 = achallenges.KeyAuthorizationAnnotatedChallenge( # pylint: disable=star-args challb=messages.ChallengeBody(**kwargs), domain="example.com", account_key="key") kwargs["chall"] = acme_util.TLSSNI01 self.tls_sni_same = achallenges.KeyAuthorizationAnnotatedChallenge( # pylint: disable=star-args challb=messages.ChallengeBody(**kwargs), domain="example.com", account_key="key") kwargs["error"] = messages.Error(typ="dnssec", detail="detail") self.tls_sni_diff = achallenges.KeyAuthorizationAnnotatedChallenge( # pylint: disable=star-args challb=messages.ChallengeBody(**kwargs), domain="foo.bar", account_key="key") @mock.patch("letsencrypt.auth_handler.zope.component.getUtility") def test_same_error_and_domain(self, mock_zope): from letsencrypt import auth_handler auth_handler._report_failed_challs([self.http01, self.tls_sni_same]) call_list = mock_zope().add_message.call_args_list self.assertTrue(len(call_list) == 1) self.assertTrue("Domain: example.com\nType: tls\nDetail: detail" in call_list[0][0][0]) @mock.patch("letsencrypt.auth_handler.zope.component.getUtility") def test_different_errors_and_domains(self, mock_zope): from letsencrypt import auth_handler auth_handler._report_failed_challs([self.http01, self.tls_sni_diff]) self.assertTrue(mock_zope().add_message.call_count == 2) def gen_auth_resp(chall_list): """Generate a dummy authorization response.""" return ["%s%s" % (chall.__class__.__name__, chall.domain) for chall in chall_list] def gen_dom_authzr(domain, unused_new_authzr_uri, challs): """Generates new authzr for domains.""" return acme_util.gen_authzr( messages.STATUS_PENDING, domain, challs, [messages.STATUS_PENDING] * len(challs)) if __name__ == "__main__": unittest.main() # pragma: no cover letsencrypt-0.4.1/letsencrypt/tests/colored_logging_test.py0000644000175000017500000000225512665157707023774 0ustar bmwbmw00000000000000"""Tests for letsencrypt.colored_logging.""" import logging import StringIO import unittest from letsencrypt import le_util class StreamHandlerTest(unittest.TestCase): """Tests for letsencrypt.colored_logging.""" def setUp(self): from letsencrypt import colored_logging self.stream = StringIO.StringIO() self.stream.isatty = lambda: True self.handler = colored_logging.StreamHandler(self.stream) self.logger = logging.getLogger() self.logger.setLevel(logging.DEBUG) self.logger.addHandler(self.handler) def test_format(self): msg = 'I did a thing' self.logger.debug(msg) self.assertEqual(self.stream.getvalue(), '{0}\n'.format(msg)) def test_format_and_red_level(self): msg = 'I did another thing' self.handler.red_level = logging.DEBUG self.logger.debug(msg) self.assertEqual(self.stream.getvalue(), '{0}{1}{2}\n'.format(le_util.ANSI_SGR_RED, msg, le_util.ANSI_SGR_RESET)) if __name__ == "__main__": unittest.main() # pragma: no cover letsencrypt-0.4.1/letsencrypt/tests/notify_test.py0000644000175000017500000000415112665157707022144 0ustar bmwbmw00000000000000"""Tests for letsencrypt.notify.""" import socket import unittest import mock class NotifyTests(unittest.TestCase): """Tests for the notifier.""" @mock.patch("letsencrypt.notify.smtplib.LMTP") def test_smtp_success(self, mock_lmtp): from letsencrypt.notify import notify lmtp_obj = mock.MagicMock() mock_lmtp.return_value = lmtp_obj self.assertTrue(notify("Goose", "auntrhody@example.com", "The old grey goose is dead.")) self.assertEqual(lmtp_obj.connect.call_count, 1) self.assertEqual(lmtp_obj.sendmail.call_count, 1) @mock.patch("letsencrypt.notify.smtplib.LMTP") @mock.patch("letsencrypt.notify.subprocess.Popen") def test_smtp_failure(self, mock_popen, mock_lmtp): from letsencrypt.notify import notify lmtp_obj = mock.MagicMock() mock_lmtp.return_value = lmtp_obj lmtp_obj.sendmail.side_effect = socket.error(17) proc = mock.MagicMock() mock_popen.return_value = proc self.assertTrue(notify("Goose", "auntrhody@example.com", "The old grey goose is dead.")) self.assertEqual(lmtp_obj.sendmail.call_count, 1) self.assertEqual(proc.communicate.call_count, 1) @mock.patch("letsencrypt.notify.smtplib.LMTP") @mock.patch("letsencrypt.notify.subprocess.Popen") def test_everything_fails(self, mock_popen, mock_lmtp): from letsencrypt.notify import notify lmtp_obj = mock.MagicMock() mock_lmtp.return_value = lmtp_obj lmtp_obj.sendmail.side_effect = socket.error(17) proc = mock.MagicMock() mock_popen.return_value = proc proc.communicate.side_effect = OSError("What we have here is a " "failure to communicate.") self.assertFalse(notify("Goose", "auntrhody@example.com", "The old grey goose is dead.")) self.assertEqual(lmtp_obj.sendmail.call_count, 1) self.assertEqual(proc.communicate.call_count, 1) if __name__ == "__main__": unittest.main() # pragma: no cover letsencrypt-0.4.1/letsencrypt/tests/reverter_test.py0000644000175000017500000004435012665157707022477 0ustar bmwbmw00000000000000"""Test letsencrypt.reverter.""" import csv import itertools import logging import os import shutil import tempfile import unittest import mock from letsencrypt import errors class ReverterCheckpointLocalTest(unittest.TestCase): # pylint: disable=too-many-instance-attributes, too-many-public-methods """Test the Reverter Class.""" def setUp(self): from letsencrypt.reverter import Reverter # Disable spurious errors... we are trying to test for them logging.disable(logging.CRITICAL) self.config = setup_work_direc() self.reverter = Reverter(self.config) tup = setup_test_files() self.config1, self.config2, self.dir1, self.dir2, self.sets = tup def tearDown(self): shutil.rmtree(self.config.work_dir) shutil.rmtree(self.dir1) shutil.rmtree(self.dir2) logging.disable(logging.NOTSET) def test_basic_add_to_temp_checkpoint(self): # These shouldn't conflict even though they are both named config.txt self.reverter.add_to_temp_checkpoint(self.sets[0], "save1") self.reverter.add_to_temp_checkpoint(self.sets[1], "save2") self.assertTrue(os.path.isdir(self.config.temp_checkpoint_dir)) self.assertEqual(get_save_notes( self.config.temp_checkpoint_dir), "save1save2") self.assertFalse(os.path.isfile( os.path.join(self.config.temp_checkpoint_dir, "NEW_FILES"))) self.assertEqual( get_filepaths(self.config.temp_checkpoint_dir), "{0}\n{1}\n".format(self.config1, self.config2)) def test_add_to_checkpoint_copy_failure(self): with mock.patch("letsencrypt.reverter.shutil.copy2") as mock_copy2: mock_copy2.side_effect = IOError("bad copy") self.assertRaises( errors.ReverterError, self.reverter.add_to_checkpoint, self.sets[0], "save1") def test_checkpoint_conflict(self): """Make sure that checkpoint errors are thrown appropriately.""" config3 = os.path.join(self.dir1, "config3.txt") self.reverter.register_file_creation(True, config3) update_file(config3, "This is a new file!") self.reverter.add_to_checkpoint(self.sets[2], "save1") # This shouldn't throw an error self.reverter.add_to_temp_checkpoint(self.sets[0], "save2") # Raise error self.assertRaises(errors.ReverterError, self.reverter.add_to_checkpoint, self.sets[2], "save3") # Should not cause an error self.reverter.add_to_checkpoint(self.sets[1], "save4") # Check to make sure new files are also checked... self.assertRaises(errors.ReverterError, self.reverter.add_to_checkpoint, set([config3]), "invalid save") def test_multiple_saves_and_temp_revert(self): self.reverter.add_to_temp_checkpoint(self.sets[0], "save1") update_file(self.config1, "updated-directive") self.reverter.add_to_temp_checkpoint(self.sets[0], "save2-updated dir") update_file(self.config1, "new directive change that we won't keep") self.reverter.revert_temporary_config() self.assertEqual(read_in(self.config1), "directive-dir1") def test_multiple_registration_fail_and_revert(self): config3 = os.path.join(self.dir1, "config3.txt") update_file(config3, "Config3") config4 = os.path.join(self.dir2, "config4.txt") update_file(config4, "Config4") # Test multiple registrations and two registrations at once self.reverter.register_file_creation(True, self.config1) self.reverter.register_file_creation(True, self.config2) self.reverter.register_file_creation(True, config3, config4) # Simulate Let's Encrypt crash... recovery routine is run self.reverter.recovery_routine() self.assertFalse(os.path.isfile(self.config1)) self.assertFalse(os.path.isfile(self.config2)) self.assertFalse(os.path.isfile(config3)) self.assertFalse(os.path.isfile(config4)) def test_multiple_registration_same_file(self): self.reverter.register_file_creation(True, self.config1) self.reverter.register_file_creation(True, self.config1) self.reverter.register_file_creation(True, self.config1) self.reverter.register_file_creation(True, self.config1) files = get_new_files(self.config.temp_checkpoint_dir) self.assertEqual(len(files), 1) def test_register_file_creation_write_error(self): m_open = mock.mock_open() with mock.patch("letsencrypt.reverter.open", m_open, create=True): m_open.side_effect = OSError("bad open") self.assertRaises( errors.ReverterError, self.reverter.register_file_creation, True, self.config1) def test_bad_registration(self): # Made this mistake and want to make sure it doesn't happen again... self.assertRaises( errors.ReverterError, self.reverter.register_file_creation, "filepath") def test_register_undo_command(self): coms = [ ["a2dismod", "ssl"], ["a2dismod", "rewrite"], ["cleanslate"] ] for com in coms: self.reverter.register_undo_command(True, com) act_coms = get_undo_commands(self.config.temp_checkpoint_dir) for a_com, com in itertools.izip(act_coms, coms): self.assertEqual(a_com, com) def test_bad_register_undo_command(self): m_open = mock.mock_open() with mock.patch("letsencrypt.reverter.open", m_open, create=True): m_open.side_effect = OSError("bad open") self.assertRaises( errors.ReverterError, self.reverter.register_undo_command, True, ["command"]) @mock.patch("letsencrypt.le_util.run_script") def test_run_undo_commands(self, mock_run): mock_run.side_effect = ["", errors.SubprocessError] coms = [ ["invalid_command"], ["a2dismod", "ssl"], ] for com in coms: self.reverter.register_undo_command(True, com) self.reverter.revert_temporary_config() self.assertEqual(mock_run.call_count, 2) def test_recovery_routine_in_progress_failure(self): self.reverter.add_to_checkpoint(self.sets[0], "perm save") # pylint: disable=protected-access self.reverter._recover_checkpoint = mock.MagicMock( side_effect=errors.ReverterError) self.assertRaises(errors.ReverterError, self.reverter.recovery_routine) def test_recover_checkpoint_revert_temp_failures(self): mock_recover = mock.MagicMock( side_effect=errors.ReverterError("e")) # pylint: disable=protected-access self.reverter._recover_checkpoint = mock_recover self.reverter.add_to_temp_checkpoint(self.sets[0], "config1 save") self.assertRaises( errors.ReverterError, self.reverter.revert_temporary_config) def test_recover_checkpoint_rollback_failure(self): mock_recover = mock.MagicMock( side_effect=errors.ReverterError("e")) # pylint: disable=protected-access self.reverter._recover_checkpoint = mock_recover self.reverter.add_to_checkpoint(self.sets[0], "config1 save") self.reverter.finalize_checkpoint("Title") self.assertRaises( errors.ReverterError, self.reverter.rollback_checkpoints, 1) def test_recover_checkpoint_copy_failure(self): self.reverter.add_to_temp_checkpoint(self.sets[0], "save1") with mock.patch("letsencrypt.reverter.shutil.copy2") as mock_copy2: mock_copy2.side_effect = OSError("bad copy") self.assertRaises( errors.ReverterError, self.reverter.revert_temporary_config) def test_recover_checkpoint_rm_failure(self): self.reverter.add_to_temp_checkpoint(self.sets[0], "temp save") with mock.patch("letsencrypt.reverter.shutil.rmtree") as mock_rmtree: mock_rmtree.side_effect = OSError("Cannot remove tree") self.assertRaises( errors.ReverterError, self.reverter.revert_temporary_config) @mock.patch("letsencrypt.reverter.logger.warning") def test_recover_checkpoint_missing_new_files(self, mock_warn): self.reverter.register_file_creation( True, os.path.join(self.dir1, "missing_file.txt")) self.reverter.revert_temporary_config() self.assertEqual(mock_warn.call_count, 1) @mock.patch("letsencrypt.reverter.os.remove") def test_recover_checkpoint_remove_failure(self, mock_remove): self.reverter.register_file_creation(True, self.config1) mock_remove.side_effect = OSError("Can't remove") self.assertRaises( errors.ReverterError, self.reverter.revert_temporary_config) def test_recovery_routine_temp_and_perm(self): # Register a new perm checkpoint file config3 = os.path.join(self.dir1, "config3.txt") self.reverter.register_file_creation(False, config3) update_file(config3, "This is a new perm file!") # Add changes to perm checkpoint self.reverter.add_to_checkpoint(self.sets[0], "perm save1") update_file(self.config1, "updated perm config1") self.reverter.add_to_checkpoint(self.sets[1], "perm save2") update_file(self.config2, "updated perm config2") # Add changes to a temporary checkpoint self.reverter.add_to_temp_checkpoint(self.sets[0], "temp save1") update_file(self.config1, "second update now temp config1") # Register a new temp checkpoint file config4 = os.path.join(self.dir2, "config4.txt") self.reverter.register_file_creation(True, config4) update_file(config4, "New temporary file!") # Now erase everything self.reverter.recovery_routine() # Now Run tests # These were new files.. they should be removed self.assertFalse(os.path.isfile(config3)) self.assertFalse(os.path.isfile(config4)) # Check to make sure everything got rolled back appropriately self.assertEqual(read_in(self.config1), "directive-dir1") self.assertEqual(read_in(self.config2), "directive-dir2") class TestFullCheckpointsReverter(unittest.TestCase): # pylint: disable=too-many-instance-attributes """Tests functions having to deal with full checkpoints.""" def setUp(self): from letsencrypt.reverter import Reverter # Disable spurious errors... logging.disable(logging.CRITICAL) self.config = setup_work_direc() self.reverter = Reverter(self.config) tup = setup_test_files() self.config1, self.config2, self.dir1, self.dir2, self.sets = tup def tearDown(self): shutil.rmtree(self.config.work_dir) shutil.rmtree(self.dir1) shutil.rmtree(self.dir2) logging.disable(logging.NOTSET) def test_rollback_improper_inputs(self): self.assertRaises( errors.ReverterError, self.reverter.rollback_checkpoints, "-1") self.assertRaises( errors.ReverterError, self.reverter.rollback_checkpoints, -1000) self.assertRaises( errors.ReverterError, self.reverter.rollback_checkpoints, "one") def test_rollback_finalize_checkpoint_valid_inputs(self): config3 = self._setup_three_checkpoints() # Check resulting backup directory self.assertEqual(len(os.listdir(self.config.backup_dir)), 3) # Check rollbacks # First rollback self.reverter.rollback_checkpoints(1) self.assertEqual(read_in(self.config1), "update config1") self.assertEqual(read_in(self.config2), "update config2") # config3 was not included in checkpoint self.assertEqual(read_in(config3), "Final form config3") # Second rollback self.reverter.rollback_checkpoints(1) self.assertEqual(read_in(self.config1), "update config1") self.assertEqual(read_in(self.config2), "directive-dir2") self.assertFalse(os.path.isfile(config3)) # One dir left... check title all_dirs = os.listdir(self.config.backup_dir) self.assertEqual(len(all_dirs), 1) self.assertTrue( "First Checkpoint" in get_save_notes( os.path.join(self.config.backup_dir, all_dirs[0]))) # Final rollback self.reverter.rollback_checkpoints(1) self.assertEqual(read_in(self.config1), "directive-dir1") def test_finalize_checkpoint_no_in_progress(self): # No need to warn for this... just make sure there are no errors. self.reverter.finalize_checkpoint("No checkpoint...") @mock.patch("letsencrypt.reverter.shutil.move") def test_finalize_checkpoint_cannot_title(self, mock_move): self.reverter.add_to_checkpoint(self.sets[0], "perm save") mock_move.side_effect = OSError("cannot move") self.assertRaises( errors.ReverterError, self.reverter.finalize_checkpoint, "Title") @mock.patch("letsencrypt.reverter.os.rename") def test_finalize_checkpoint_no_rename_directory(self, mock_rename): self.reverter.add_to_checkpoint(self.sets[0], "perm save") mock_rename.side_effect = OSError self.assertRaises( errors.ReverterError, self.reverter.finalize_checkpoint, "Title") @mock.patch("letsencrypt.reverter.logger") def test_rollback_too_many(self, mock_logger): # Test no exist warning... self.reverter.rollback_checkpoints(1) self.assertEqual(mock_logger.warning.call_count, 1) # Test Generic warning mock_logger.warning.call_count = 0 self._setup_three_checkpoints() self.reverter.rollback_checkpoints(4) self.assertEqual(mock_logger.warning.call_count, 1) def test_multi_rollback(self): config3 = self._setup_three_checkpoints() self.reverter.rollback_checkpoints(3) self.assertEqual(read_in(self.config1), "directive-dir1") self.assertEqual(read_in(self.config2), "directive-dir2") self.assertFalse(os.path.isfile(config3)) @mock.patch("letsencrypt.reverter.zope.component.getUtility") def test_view_config_changes(self, mock_output): """This is not strict as this is subject to change.""" self._setup_three_checkpoints() # Make sure it doesn't throw any errors self.reverter.view_config_changes() # Make sure notification is output self.assertEqual(mock_output().notification.call_count, 1) @mock.patch("letsencrypt.reverter.logger") def test_view_config_changes_no_backups(self, mock_logger): self.reverter.view_config_changes() self.assertTrue(mock_logger.info.call_count > 0) def test_view_config_changes_bad_backups_dir(self): # There shouldn't be any "in progess directories when this is called # It must just be clean checkpoints os.makedirs(os.path.join(self.config.backup_dir, "in_progress")) self.assertRaises( errors.ReverterError, self.reverter.view_config_changes) def test_view_config_changes_for_logging(self): self._setup_three_checkpoints() config_changes = self.reverter.view_config_changes(for_logging=True) self.assertTrue("First Checkpoint" in config_changes) self.assertTrue("Second Checkpoint" in config_changes) self.assertTrue("Third Checkpoint" in config_changes) def _setup_three_checkpoints(self): """Generate some finalized checkpoints.""" # Checkpoint1 - config1 self.reverter.add_to_checkpoint(self.sets[0], "first save") self.reverter.finalize_checkpoint("First Checkpoint") update_file(self.config1, "update config1") # Checkpoint2 - new file config3, update config2 config3 = os.path.join(self.dir1, "config3.txt") self.reverter.register_file_creation(False, config3) update_file(config3, "directive-config3") self.reverter.add_to_checkpoint(self.sets[1], "second save") self.reverter.finalize_checkpoint("Second Checkpoint") update_file(self.config2, "update config2") update_file(config3, "update config3") # Checkpoint3 - update config1, config2 self.reverter.add_to_checkpoint(self.sets[2], "third save") self.reverter.finalize_checkpoint("Third Checkpoint - Save both") update_file(self.config1, "Final form config1") update_file(self.config2, "Final form config2") update_file(config3, "Final form config3") return config3 def setup_work_direc(): """Setup directories. :returns: Mocked :class:`letsencrypt.interfaces.IConfig` """ work_dir = tempfile.mkdtemp("work") backup_dir = os.path.join(work_dir, "backup") return mock.MagicMock( work_dir=work_dir, backup_dir=backup_dir, temp_checkpoint_dir=os.path.join(work_dir, "temp"), in_progress_dir=os.path.join(backup_dir, "in_progress_dir")) def setup_test_files(): """Setup sample configuration files.""" dir1 = tempfile.mkdtemp("dir1") dir2 = tempfile.mkdtemp("dir2") config1 = os.path.join(dir1, "config.txt") config2 = os.path.join(dir2, "config.txt") with open(config1, "w") as file_fd: file_fd.write("directive-dir1") with open(config2, "w") as file_fd: file_fd.write("directive-dir2") sets = [set([config1]), set([config2]), set([config1, config2])] return config1, config2, dir1, dir2, sets def get_save_notes(dire): """Read save notes""" return read_in(os.path.join(dire, "CHANGES_SINCE")) def get_filepaths(dire): """Get Filepaths""" return read_in(os.path.join(dire, "FILEPATHS")) def get_new_files(dire): """Get new files.""" return read_in(os.path.join(dire, "NEW_FILES")).splitlines() def get_undo_commands(dire): """Get new files.""" with open(os.path.join(dire, "COMMANDS")) as csvfile: return list(csv.reader(csvfile)) def read_in(path): """Read in a file, return the str""" with open(path, "r") as file_fd: return file_fd.read() def update_file(filename, string): """Update a file with a new value.""" with open(filename, "w") as file_fd: file_fd.write(string) if __name__ == "__main__": unittest.main() # pragma: no cover letsencrypt-0.4.1/letsencrypt/tests/acme_util.py0000644000175000017500000001117112665157707021537 0ustar bmwbmw00000000000000"""ACME utilities for testing.""" import datetime import itertools from acme import challenges from acme import jose from acme import messages from letsencrypt.tests import test_util KEY = test_util.load_rsa_private_key('rsa512_key.pem') # Challenges HTTP01 = challenges.HTTP01( token="evaGxfADs6pSRb2LAv9IZf17Dt3juxGJ+PCt92wr+oA") TLSSNI01 = challenges.TLSSNI01( token=jose.b64decode(b"evaGxfADs6pSRb2LAv9IZf17Dt3juxGJyPCt92wrDoA")) DNS = challenges.DNS(token="17817c66b60ce2e4012dfad92657527a") RECOVERY_CONTACT = challenges.RecoveryContact( activation_url="https://example.ca/sendrecovery/a5bd99383fb0", success_url="https://example.ca/confirmrecovery/bb1b9928932", contact="c********n@example.com") POP = challenges.ProofOfPossession( alg="RS256", nonce=jose.b64decode("eET5udtV7aoX8Xl8gYiZIA"), hints=challenges.ProofOfPossession.Hints( jwk=jose.JWKRSA(key=KEY.public_key()), cert_fingerprints=( "93416768eb85e33adc4277f4c9acd63e7418fcfe", "16d95b7b63f1972b980b14c20291f3c0d1855d95", "48b46570d9fc6358108af43ad1649484def0debf" ), certs=(), # TODO subject_key_identifiers=("d0083162dcc4c8a23ecb8aecbd86120e56fd24e5"), serial_numbers=(34234239832, 23993939911, 17), issuers=( "C=US, O=SuperT LLC, CN=SuperTrustworthy Public CA", "O=LessTrustworthy CA Inc, CN=LessTrustworthy But StillSecure", ), authorized_for=("www.example.com", "example.net"), ) ) CHALLENGES = [HTTP01, TLSSNI01, DNS, RECOVERY_CONTACT, POP] DV_CHALLENGES = [chall for chall in CHALLENGES if isinstance(chall, challenges.DVChallenge)] CONT_CHALLENGES = [chall for chall in CHALLENGES if isinstance(chall, challenges.ContinuityChallenge)] def gen_combos(challbs): """Generate natural combinations for challbs.""" dv_chall = [] cont_chall = [] for i, challb in enumerate(challbs): # pylint: disable=redefined-outer-name if isinstance(challb.chall, challenges.DVChallenge): dv_chall.append(i) else: cont_chall.append(i) # Gen combos for 1 of each type, lowest index first (makes testing easier) return tuple((i, j) if i < j else (j, i) for i in dv_chall for j in cont_chall) def chall_to_challb(chall, status): # pylint: disable=redefined-outer-name """Return ChallengeBody from Challenge.""" kwargs = { "chall": chall, "uri": chall.typ + "_uri", "status": status, } if status == messages.STATUS_VALID: kwargs.update({"validated": datetime.datetime.now()}) return messages.ChallengeBody(**kwargs) # pylint: disable=star-args # Pending ChallengeBody objects TLSSNI01_P = chall_to_challb(TLSSNI01, messages.STATUS_PENDING) HTTP01_P = chall_to_challb(HTTP01, messages.STATUS_PENDING) DNS_P = chall_to_challb(DNS, messages.STATUS_PENDING) RECOVERY_CONTACT_P = chall_to_challb(RECOVERY_CONTACT, messages.STATUS_PENDING) POP_P = chall_to_challb(POP, messages.STATUS_PENDING) CHALLENGES_P = [HTTP01_P, TLSSNI01_P, DNS_P, RECOVERY_CONTACT_P, POP_P] DV_CHALLENGES_P = [challb for challb in CHALLENGES_P if isinstance(challb.chall, challenges.DVChallenge)] CONT_CHALLENGES_P = [ challb for challb in CHALLENGES_P if isinstance(challb.chall, challenges.ContinuityChallenge) ] def gen_authzr(authz_status, domain, challs, statuses, combos=True): """Generate an authorization resource. :param authz_status: Status object :type authz_status: :class:`acme.messages.Status` :param list challs: Challenge objects :param list statuses: status of each challenge object :param bool combos: Whether or not to add combinations """ # pylint: disable=redefined-outer-name challbs = tuple( chall_to_challb(chall, status) for chall, status in itertools.izip(challs, statuses) ) authz_kwargs = { "identifier": messages.Identifier( typ=messages.IDENTIFIER_FQDN, value=domain), "challenges": challbs, } if combos: authz_kwargs.update({"combinations": gen_combos(challbs)}) if authz_status == messages.STATUS_VALID: authz_kwargs.update({ "status": authz_status, "expires": datetime.datetime.now() + datetime.timedelta(days=31), }) else: authz_kwargs.update({ "status": authz_status, }) # pylint: disable=star-args return messages.AuthorizationResource( uri="https://trusted.ca/new-authz-resource", new_cert_uri="https://trusted.ca/new-cert", body=messages.Authorization(**authz_kwargs) ) letsencrypt-0.4.1/letsencrypt/tests/configuration_test.py0000644000175000017500000001301312665157707023500 0ustar bmwbmw00000000000000"""Tests for letsencrypt.configuration.""" import os import unittest import mock from letsencrypt import errors class NamespaceConfigTest(unittest.TestCase): """Tests for letsencrypt.configuration.NamespaceConfig.""" def setUp(self): self.namespace = mock.MagicMock( config_dir='/tmp/config', work_dir='/tmp/foo', foo='bar', server='https://acme-server.org:443/new', tls_sni_01_port=1234, http01_port=4321) from letsencrypt.configuration import NamespaceConfig self.config = NamespaceConfig(self.namespace) def test_init_same_ports(self): self.namespace.tls_sni_01_port = 4321 from letsencrypt.configuration import NamespaceConfig self.assertRaises(errors.Error, NamespaceConfig, self.namespace) def test_proxy_getattr(self): self.assertEqual(self.config.foo, 'bar') self.assertEqual(self.config.work_dir, '/tmp/foo') def test_server_path(self): self.assertEqual(['acme-server.org:443', 'new'], self.config.server_path.split(os.path.sep)) self.namespace.server = ('http://user:pass@acme.server:443' '/p/a/t/h;parameters?query#fragment') self.assertEqual(['user:pass@acme.server:443', 'p', 'a', 't', 'h'], self.config.server_path.split(os.path.sep)) @mock.patch('letsencrypt.configuration.constants') def test_dynamic_dirs(self, constants): constants.ACCOUNTS_DIR = 'acc' constants.BACKUP_DIR = 'backups' constants.CSR_DIR = 'csr' constants.IN_PROGRESS_DIR = '../p' constants.KEY_DIR = 'keys' constants.TEMP_CHECKPOINT_DIR = 't' self.assertEqual( self.config.accounts_dir, '/tmp/config/acc/acme-server.org:443/new') self.assertEqual(self.config.backup_dir, '/tmp/foo/backups') self.assertEqual(self.config.csr_dir, '/tmp/config/csr') self.assertEqual(self.config.in_progress_dir, '/tmp/foo/../p') self.assertEqual(self.config.key_dir, '/tmp/config/keys') self.assertEqual(self.config.temp_checkpoint_dir, '/tmp/foo/t') def test_absolute_paths(self): from letsencrypt.configuration import NamespaceConfig config_base = "foo" work_base = "bar" logs_base = "baz" mock_namespace = mock.MagicMock(spec=['config_dir', 'work_dir', 'logs_dir', 'http01_port', 'tls_sni_01_port', 'domains', 'server']) mock_namespace.config_dir = config_base mock_namespace.work_dir = work_base mock_namespace.logs_dir = logs_base config = NamespaceConfig(mock_namespace) self.assertTrue(os.path.isabs(config.config_dir)) self.assertEqual(config.config_dir, os.path.join(os.getcwd(), config_base)) self.assertTrue(os.path.isabs(config.work_dir)) self.assertEqual(config.work_dir, os.path.join(os.getcwd(), work_base)) self.assertTrue(os.path.isabs(config.logs_dir)) self.assertEqual(config.logs_dir, os.path.join(os.getcwd(), logs_base)) self.assertTrue(os.path.isabs(config.accounts_dir)) self.assertTrue(os.path.isabs(config.backup_dir)) self.assertTrue(os.path.isabs(config.csr_dir)) self.assertTrue(os.path.isabs(config.in_progress_dir)) self.assertTrue(os.path.isabs(config.key_dir)) self.assertTrue(os.path.isabs(config.temp_checkpoint_dir)) class RenewerConfigurationTest(unittest.TestCase): """Test for letsencrypt.configuration.RenewerConfiguration.""" def setUp(self): self.namespace = mock.MagicMock(config_dir='/tmp/config') from letsencrypt.configuration import RenewerConfiguration self.config = RenewerConfiguration(self.namespace) @mock.patch('letsencrypt.configuration.constants') def test_dynamic_dirs(self, constants): constants.ARCHIVE_DIR = 'a' constants.LIVE_DIR = 'l' constants.RENEWAL_CONFIGS_DIR = 'renewal_configs' constants.RENEWER_CONFIG_FILENAME = 'r.conf' self.assertEqual(self.config.archive_dir, '/tmp/config/a') self.assertEqual(self.config.live_dir, '/tmp/config/l') self.assertEqual( self.config.renewal_configs_dir, '/tmp/config/renewal_configs') self.assertEqual(self.config.renewer_config_file, '/tmp/config/r.conf') def test_absolute_paths(self): from letsencrypt.configuration import NamespaceConfig from letsencrypt.configuration import RenewerConfiguration config_base = "foo" work_base = "bar" logs_base = "baz" mock_namespace = mock.MagicMock(spec=['config_dir', 'work_dir', 'logs_dir', 'http01_port', 'tls_sni_01_port', 'domains', 'server']) mock_namespace.config_dir = config_base mock_namespace.work_dir = work_base mock_namespace.logs_dir = logs_base config = RenewerConfiguration(NamespaceConfig(mock_namespace)) self.assertTrue(os.path.isabs(config.archive_dir)) self.assertTrue(os.path.isabs(config.live_dir)) self.assertTrue(os.path.isabs(config.renewal_configs_dir)) self.assertTrue(os.path.isabs(config.renewer_config_file)) if __name__ == '__main__': unittest.main() # pragma: no cover letsencrypt-0.4.1/letsencrypt/tests/account_test.py0000644000175000017500000001477512665157707022305 0ustar bmwbmw00000000000000"""Tests for letsencrypt.account.""" import datetime import os import shutil import stat import tempfile import unittest import mock import pytz from acme import jose from acme import messages from letsencrypt import errors from letsencrypt.tests import test_util KEY = jose.JWKRSA.load(test_util.load_vector("rsa512_key_2.pem")) class AccountTest(unittest.TestCase): """Tests for letsencrypt.account.Account.""" def setUp(self): from letsencrypt.account import Account self.regr = mock.MagicMock() self.meta = Account.Meta( creation_host="test.letsencrypt.org", creation_dt=datetime.datetime( 2015, 7, 4, 14, 4, 10, tzinfo=pytz.UTC)) self.acc = Account(self.regr, KEY, self.meta) with mock.patch("letsencrypt.account.socket") as mock_socket: mock_socket.getfqdn.return_value = "test.letsencrypt.org" with mock.patch("letsencrypt.account.datetime") as mock_dt: mock_dt.datetime.now.return_value = self.meta.creation_dt self.acc_no_meta = Account(self.regr, KEY) def test_init(self): self.assertEqual(self.regr, self.acc.regr) self.assertEqual(KEY, self.acc.key) self.assertEqual(self.meta, self.acc_no_meta.meta) def test_id(self): self.assertEqual( self.acc.id, "bca5889f66457d5b62fbba7b25f9ab6f") def test_slug(self): self.assertEqual( self.acc.slug, "test.letsencrypt.org@2015-07-04T14:04:10Z (bca5)") def test_repr(self): self.assertEqual( repr(self.acc), "") class ReportNewAccountTest(unittest.TestCase): """Tests for letsencrypt.account.report_new_account.""" def setUp(self): self.config = mock.MagicMock(config_dir="/etc/letsencrypt") reg = messages.Registration.from_data(email="rhino@jungle.io") self.acc = mock.MagicMock(regr=messages.RegistrationResource( uri=None, new_authzr_uri=None, body=reg)) def _call(self): from letsencrypt.account import report_new_account report_new_account(self.acc, self.config) @mock.patch("letsencrypt.account.zope.component.queryUtility") def test_no_reporter(self, mock_zope): mock_zope.return_value = None self._call() @mock.patch("letsencrypt.account.zope.component.queryUtility") def test_it(self, mock_zope): self._call() call_list = mock_zope().add_message.call_args_list self.assertTrue(self.config.config_dir in call_list[0][0][0]) self.assertTrue( ", ".join(self.acc.regr.body.emails) in call_list[1][0][0]) class AccountMemoryStorageTest(unittest.TestCase): """Tests for letsencrypt.account.AccountMemoryStorage.""" def setUp(self): from letsencrypt.account import AccountMemoryStorage self.storage = AccountMemoryStorage() def test_it(self): account = mock.Mock(id="x") self.assertEqual([], self.storage.find_all()) self.assertRaises(errors.AccountNotFound, self.storage.load, "x") self.storage.save(account) self.assertEqual([account], self.storage.find_all()) self.assertEqual(account, self.storage.load("x")) self.storage.save(account) self.assertEqual([account], self.storage.find_all()) class AccountFileStorageTest(unittest.TestCase): """Tests for letsencrypt.account.AccountFileStorage.""" def setUp(self): self.tmp = tempfile.mkdtemp() self.config = mock.MagicMock( accounts_dir=os.path.join(self.tmp, "accounts")) from letsencrypt.account import AccountFileStorage self.storage = AccountFileStorage(self.config) from letsencrypt.account import Account self.acc = Account( regr=messages.RegistrationResource( uri=None, new_authzr_uri=None, body=messages.Registration()), key=KEY) def tearDown(self): shutil.rmtree(self.tmp) def test_init_creates_dir(self): self.assertTrue(os.path.isdir(self.config.accounts_dir)) def test_save_and_restore(self): self.storage.save(self.acc) account_path = os.path.join(self.config.accounts_dir, self.acc.id) self.assertTrue(os.path.exists(account_path)) for file_name in "regr.json", "meta.json", "private_key.json": self.assertTrue(os.path.exists( os.path.join(account_path, file_name))) self.assertEqual("0400", oct(os.stat(os.path.join( account_path, "private_key.json"))[stat.ST_MODE] & 0o777)) # restore self.assertEqual(self.acc, self.storage.load(self.acc.id)) def test_find_all(self): self.storage.save(self.acc) self.assertEqual([self.acc], self.storage.find_all()) def test_find_all_none_empty_list(self): self.assertEqual([], self.storage.find_all()) def test_find_all_accounts_dir_absent(self): os.rmdir(self.config.accounts_dir) self.assertEqual([], self.storage.find_all()) def test_find_all_load_skips(self): self.storage.load = mock.MagicMock( side_effect=["x", errors.AccountStorageError, "z"]) with mock.patch("letsencrypt.account.os.listdir") as mock_listdir: mock_listdir.return_value = ["x", "y", "z"] self.assertEqual(["x", "z"], self.storage.find_all()) def test_load_non_existent_raises_error(self): self.assertRaises(errors.AccountNotFound, self.storage.load, "missing") def test_load_id_mismatch_raises_error(self): self.storage.save(self.acc) shutil.move(os.path.join(self.config.accounts_dir, self.acc.id), os.path.join(self.config.accounts_dir, "x" + self.acc.id)) self.assertRaises(errors.AccountStorageError, self.storage.load, "x" + self.acc.id) def test_load_ioerror(self): self.storage.save(self.acc) mock_open = mock.mock_open() mock_open.side_effect = IOError with mock.patch("__builtin__.open", mock_open): self.assertRaises( errors.AccountStorageError, self.storage.load, self.acc.id) def test_save_ioerrors(self): mock_open = mock.mock_open() mock_open.side_effect = IOError # TODO: [None, None, IOError] with mock.patch("__builtin__.open", mock_open): self.assertRaises( errors.AccountStorageError, self.storage.save, self.acc) if __name__ == "__main__": unittest.main() # pragma: no cover letsencrypt-0.4.1/letsencrypt/tests/proof_of_possession_test.py0000644000175000017500000000663412665157707024742 0ustar bmwbmw00000000000000"""Tests for letsencrypt.proof_of_possession.""" import os import tempfile import unittest import mock from acme import challenges from acme import jose from acme import messages from letsencrypt import achallenges from letsencrypt import proof_of_possession from letsencrypt.display import util as display_util from letsencrypt.tests import test_util CERT0_PATH = test_util.vector_path("cert.der") CERT2_PATH = test_util.vector_path("dsa_cert.pem") CERT2_KEY_PATH = test_util.vector_path("dsa512_key.pem") CERT3_PATH = test_util.vector_path("matching_cert.pem") CERT3_KEY_PATH = test_util.vector_path("rsa512_key_2.pem") CERT3_KEY = test_util.load_rsa_private_key("rsa512_key_2.pem").public_key() class ProofOfPossessionTest(unittest.TestCase): def setUp(self): self.installer = mock.MagicMock() self.cert1_path = tempfile.mkstemp()[1] certs = [CERT0_PATH, self.cert1_path, CERT2_PATH, CERT3_PATH] keys = [None, None, CERT2_KEY_PATH, CERT3_KEY_PATH] self.installer.get_all_certs_keys.return_value = zip( certs, keys, 4 * [None]) self.proof_of_pos = proof_of_possession.ProofOfPossession( self.installer) hints = challenges.ProofOfPossession.Hints( jwk=jose.JWKRSA(key=CERT3_KEY), cert_fingerprints=(), certs=(), serial_numbers=(), subject_key_identifiers=(), issuers=(), authorized_for=()) chall = challenges.ProofOfPossession( alg=jose.RS256, nonce='zczv4HMLVe_0kimJ25Juig', hints=hints) challb = messages.ChallengeBody( chall=chall, uri="http://example", status=messages.STATUS_PENDING) self.achall = achallenges.ProofOfPossession( challb=challb, domain="example.com") def tearDown(self): os.remove(self.cert1_path) def test_perform_bad_challenge(self): hints = challenges.ProofOfPossession.Hints( jwk=jose.jwk.JWKOct(key="foo"), cert_fingerprints=(), certs=(), serial_numbers=(), subject_key_identifiers=(), issuers=(), authorized_for=()) chall = challenges.ProofOfPossession( alg=jose.HS512, nonce='zczv4HMLVe_0kimJ25Juig', hints=hints) challb = messages.ChallengeBody( chall=chall, uri="http://example", status=messages.STATUS_PENDING) self.achall = achallenges.ProofOfPossession( challb=challb, domain="example.com") self.assertEqual(self.proof_of_pos.perform(self.achall), None) def test_perform_no_input(self): self.assertTrue(self.proof_of_pos.perform(self.achall).verify()) @mock.patch("letsencrypt.proof_of_possession.zope.component.getUtility") def test_perform_with_input(self, mock_input): # Remove the matching certificate self.installer.get_all_certs_keys.return_value.pop() mock_input().input.side_effect = [(display_util.CANCEL, ""), (display_util.OK, CERT0_PATH), (display_util.OK, "imaginary_file"), (display_util.OK, CERT3_KEY_PATH)] self.assertFalse(self.proof_of_pos.perform(self.achall)) self.assertFalse(self.proof_of_pos.perform(self.achall)) self.assertFalse(self.proof_of_pos.perform(self.achall)) self.assertTrue(self.proof_of_pos.perform(self.achall).verify()) if __name__ == "__main__": unittest.main() # pragma: no cover letsencrypt-0.4.1/letsencrypt/tests/testdata/0000755000175000017500000000000012665157717021034 5ustar bmwbmw00000000000000letsencrypt-0.4.1/letsencrypt/tests/testdata/live/0000755000175000017500000000000012665157717021773 5ustar bmwbmw00000000000000letsencrypt-0.4.1/letsencrypt/tests/testdata/live/sample-renewal/0000755000175000017500000000000012665157717024707 5ustar bmwbmw00000000000000letsencrypt-0.4.1/letsencrypt/tests/testdata/live/sample-renewal/chain.pem0000777000175000017500000000000012665157707035321 2../../archive/sample-renewal/chain1.pemustar bmwbmw00000000000000letsencrypt-0.4.1/letsencrypt/tests/testdata/live/sample-renewal/fullchain.pem0000777000175000017500000000000012665157707037067 2../../archive/sample-renewal/fullchain1.pemustar bmwbmw00000000000000letsencrypt-0.4.1/letsencrypt/tests/testdata/live/sample-renewal/cert.pem0000777000175000017500000000000012665157707035047 2../../archive/sample-renewal/cert1.pemustar bmwbmw00000000000000letsencrypt-0.4.1/letsencrypt/tests/testdata/live/sample-renewal/privkey.pem0000777000175000017500000000000012665157707036337 2../../archive/sample-renewal/privkey1.pemustar bmwbmw00000000000000letsencrypt-0.4.1/letsencrypt/tests/testdata/csr.pem0000644000175000017500000000104612665157707022326 0ustar bmwbmw00000000000000-----BEGIN CERTIFICATE REQUEST----- MIIBXTCCAQcCAQAweTELMAkGA1UEBhMCVVMxETAPBgNVBAgMCE1pY2hpZ2FuMRIw EAYDVQQHDAlBbm4gQXJib3IxDDAKBgNVBAoMA0VGRjEfMB0GA1UECwwWVW5pdmVy c2l0eSBvZiBNaWNoaWdhbjEUMBIGA1UEAwwLZXhhbXBsZS5jb20wXDANBgkqhkiG 9w0BAQEFAANLADBIAkEArHVztFHtH92ucFJD/N/HW9AsdRsUuHUBBBDlHwNlRd3f p580rv2+6QWE30cWgdmJS86ObRz6lUTor4R0T+3C5QIDAQABoCkwJwYJKoZIhvcN AQkOMRowGDAWBgNVHREEDzANggtleGFtcGxlLmNvbTANBgkqhkiG9w0BAQsFAANB AHJH/O6BtC9aGzEVCMGOZ7z9iIRHWSzr9x/bOzn7hLwsbXPAgO1QxEwL+X+4g20G n9XBE1N9W6HCIEut2d8wACg= -----END CERTIFICATE REQUEST----- letsencrypt-0.4.1/letsencrypt/tests/testdata/sample-renewal.conf0000755000175000017500000000362212665157707024624 0ustar bmwbmw00000000000000cert = MAGICDIR/live/sample-renewal/cert.pem privkey = MAGICDIR/live/sample-renewal/privkey.pem chain = MAGICDIR/live/sample-renewal/chain.pem fullchain = MAGICDIR/live/sample-renewal/fullchain.pem renew_before_expiry = 4 years # Options and defaults used in the renewal process [renewalparams] no_self_upgrade = False apache_enmod = a2enmod no_verify_ssl = False ifaces = None apache_dismod = a2dismod register_unsafely_without_email = False apache_handle_modules = True uir = None installer = none nginx_ctl = nginx config_dir = MAGICDIR text_mode = False func = staging = True prepare = False work_dir = /var/lib/letsencrypt tos = False init = False http01_port = 80 duplicate = False noninteractive_mode = True key_path = None nginx = False nginx_server_root = /etc/nginx fullchain_path = /home/ubuntu/letsencrypt/chain.pem email = None csr = None agree_dev_preview = None redirect = None verb = certonly verbose_count = -3 config_file = None renew_by_default = False hsts = False apache_handle_sites = True authenticator = standalone domains = isnot.org, rsa_key_size = 2048 apache_challenge_location = /etc/apache2 checkpoints = 1 manual_test_mode = False apache = False cert_path = /home/ubuntu/letsencrypt/cert.pem webroot_path = None reinstall = False expand = False strict_permissions = False apache_server_root = /etc/apache2 account = None dry_run = False manual_public_ip_logging_ok = False chain_path = /home/ubuntu/letsencrypt/chain.pem break_my_certs = False standalone = True manual = False server = https://acme-staging.api.letsencrypt.org/directory standalone_supported_challenges = "tls-sni-01,http-01" webroot = False os_packages_only = False apache_init_script = None user_agent = None apache_le_vhost_ext = -le-ssl.conf debug = False tls_sni_01_port = 443 logs_dir = /var/log/letsencrypt apache_vhost_root = /etc/apache2/sites-available configurator = None [[webroot_map]] letsencrypt-0.4.1/letsencrypt/tests/testdata/webrootconftest.ini0000644000175000017500000000006612665157707024765 0ustar bmwbmw00000000000000webroot webroot-path = /tmp domains = eg.com, eg2.com letsencrypt-0.4.1/letsencrypt/tests/testdata/cert-san.pem0000644000175000017500000000142212665157707023251 0ustar bmwbmw00000000000000-----BEGIN CERTIFICATE----- MIICFjCCAcCgAwIBAgICBTkwDQYJKoZIhvcNAQELBQAwdzELMAkGA1UEBhMCVVMx ETAPBgNVBAgMCE1pY2hpZ2FuMRIwEAYDVQQHDAlBbm4gQXJib3IxKzApBgNVBAoM IlVuaXZlcnNpdHkgb2YgTWljaGlnYW4gYW5kIHRoZSBFRkYxFDASBgNVBAMMC2V4 YW1wbGUuY29tMB4XDTE0MTIxMTIyMzQ0NVoXDTE0MTIxODIyMzQ0NVowdzELMAkG A1UEBhMCVVMxETAPBgNVBAgMCE1pY2hpZ2FuMRIwEAYDVQQHDAlBbm4gQXJib3Ix KzApBgNVBAoMIlVuaXZlcnNpdHkgb2YgTWljaGlnYW4gYW5kIHRoZSBFRkYxFDAS BgNVBAMMC2V4YW1wbGUuY29tMFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBAKx1c7RR 7R/drnBSQ/zfx1vQLHUbFLh1AQQQ5R8DZUXd36efNK79vukFhN9HFoHZiUvOjm0c +pVE6K+EdE/twuUCAwEAAaM2MDQwCQYDVR0TBAIwADAnBgNVHREEIDAeggtleGFt cGxlLmNvbYIPd3d3LmV4YW1wbGUuY29tMA0GCSqGSIb3DQEBCwUAA0EASuvNKFTF nTJsvnSXn52f4BMZJJ2id/kW7+r+FJRm+L20gKQ1aqq8d3e/lzRUrv5SMf1TAOe7 RDjyGMKy5ZgM2w== -----END CERTIFICATE----- letsencrypt-0.4.1/letsencrypt/tests/testdata/rsa512_key.pem0000644000175000017500000000075512665157707023432 0ustar bmwbmw00000000000000-----BEGIN RSA PRIVATE KEY----- MIIBOgIBAAJBAKx1c7RR7R/drnBSQ/zfx1vQLHUbFLh1AQQQ5R8DZUXd36efNK79 vukFhN9HFoHZiUvOjm0c+pVE6K+EdE/twuUCAwEAAQJAMbrEnJCrQe8YqAbw1/Bn elAzIamndfE3U8bTavf9sgFpS4HL83rhd6PDbvx81ucaJAT/5x048fM/nFl4fzAc mQIhAOF/a9o3EIsDKEmUl+Z1OaOiUxDF3kqWSmALEsmvDhwXAiEAw8ljV5RO/rUp Zu2YMDFq3MKpyyMgBIJ8CxmGRc6gCmMCIGRQzkcmhfqBrhOFwkmozrqIBRIKJIjj 8TRm2LXWZZ2DAiAqVO7PztdNpynugUy4jtbGKKjBrTSNBRGA7OHlUgm0dQIhALQq 6oGU29Vxlvt3k0vmiRKU4AVfLyNXIGtcWcNG46h/ -----END RSA PRIVATE KEY----- letsencrypt-0.4.1/letsencrypt/tests/testdata/csr-nosans.pem0000644000175000017500000000070412665157707023625 0ustar bmwbmw00000000000000-----BEGIN CERTIFICATE REQUEST----- MIIBFTCBwAIBADBbMQswCQYDVQQGEwJBVTETMBEGA1UECAwKU29tZS1TdGF0ZTEh MB8GA1UECgwYSW50ZXJuZXQgV2lkZ2l0cyBQdHkgTHRkMRQwEgYDVQQDDAtleGFt cGxlLm9yZzBcMA0GCSqGSIb3DQEBAQUAA0sAMEgCQQD0thFxUTc2v6qV55wRxfwn BUOeN4bVfu5ywJqy65kzR7T1yZi5TPEiQyM7/3HgBVy9ddFc8RX4vNZaR+ROXNEz AgMBAAGgADANBgkqhkiG9w0BAQsFAANBAMikGL8Ch7hQCStXH7chhDp6+pt2+VSo wgsrPQ2Bw4veDMlSemUrH+4e0TwbbntHfvXTDHWs9P3BiIDJLxFrjuA= -----END CERTIFICATE REQUEST----- letsencrypt-0.4.1/letsencrypt/tests/testdata/dsa_cert.pem0000644000175000017500000000175512665157707023332 0ustar bmwbmw00000000000000-----BEGIN CERTIFICATE----- MIICuDCCAnWgAwIBAgIJAPjmErVMzwVLMAsGCWCGSAFlAwQDAjB3MQswCQYDVQQG EwJVUzERMA8GA1UECAwITWljaGlnYW4xEjAQBgNVBAcMCUFubiBBcmJvcjErMCkG A1UECgwiVW5pdmVyc2l0eSBvZiBNaWNoaWdhbiBhbmQgdGhlIEVGRjEUMBIGA1UE AwwLZXhhbXBsZS5jb20wHhcNMTUwNTEyMTUzOTQzWhcNMTUwNjExMTUzOTQzWjB3 MQswCQYDVQQGEwJVUzERMA8GA1UECAwITWljaGlnYW4xEjAQBgNVBAcMCUFubiBB cmJvcjErMCkGA1UECgwiVW5pdmVyc2l0eSBvZiBNaWNoaWdhbiBhbmQgdGhlIEVG RjEUMBIGA1UEAwwLZXhhbXBsZS5jb20wgfEwgakGByqGSM44BAEwgZ0CQQDB5sSg YF+iQpB4AscecBkxDBhTfkgsQF1XyhSbO/uqlJVSgeKHKp+foYI6LEApI/wQlhxO KUio9sVt8XI4+VsvAhUA2gUcQOJCCScC8qsbvykfMAl1BI8CQQCp+RrkGeX4J4Qy nNVkas5WpkT8sV1kr15Ppi1aPOq0iR/eHBdRXEmxOcEbjGat++XjWmA0mmC731g5 io+lLSDsA0MAAkBNDYtTOMZBIzpSWNw9jkjY4P1MeRRH2Qfa22HNl3vRSgj1u2tV pOLOCphKG6iT3iCVJA0rQf3YmBSTexwk9oCQo1AwTjAdBgNVHQ4EFgQUZ2DlTDGU PMwTUt0KztM6IyX61BcwHwYDVR0jBBgwFoAUZ2DlTDGUPMwTUt0KztM6IyX61Bcw DAYDVR0TBAUwAwEB/zALBglghkgBZQMEAwIDMAAwLQIVAIbMgGx+KwBr4rgqZ2Lh AAO8TegHAhQsuxpIIIphiReoWEtEJk4TqEIz/A== -----END CERTIFICATE----- letsencrypt-0.4.1/letsencrypt/tests/testdata/dsa512_key.pem0000644000175000017500000000125412665157707023407 0ustar bmwbmw00000000000000-----BEGIN DSA PARAMETERS----- MIGdAkEAwebEoGBfokKQeALHHnAZMQwYU35ILEBdV8oUmzv7qpSVUoHihyqfn6GC OixAKSP8EJYcTilIqPbFbfFyOPlbLwIVANoFHEDiQgknAvKrG78pHzAJdQSPAkEA qfka5Bnl+CeEMpzVZGrOVqZE/LFdZK9eT6YtWjzqtIkf3hwXUVxJsTnBG4xmrfvl 41pgNJpgu99YOYqPpS0g7A== -----END DSA PARAMETERS----- -----BEGIN DSA PRIVATE KEY----- MIH5AgEAAkEAwebEoGBfokKQeALHHnAZMQwYU35ILEBdV8oUmzv7qpSVUoHihyqf n6GCOixAKSP8EJYcTilIqPbFbfFyOPlbLwIVANoFHEDiQgknAvKrG78pHzAJdQSP AkEAqfka5Bnl+CeEMpzVZGrOVqZE/LFdZK9eT6YtWjzqtIkf3hwXUVxJsTnBG4xm rfvl41pgNJpgu99YOYqPpS0g7AJATQ2LUzjGQSM6UljcPY5I2OD9THkUR9kH2tth zZd70UoI9btrVaTizgqYShuok94glSQNK0H92JgUk3scJPaAkAIVAMDn61h6vrCE mNv063So6E+eYaIN -----END DSA PRIVATE KEY----- letsencrypt-0.4.1/letsencrypt/tests/testdata/matching_cert.pem0000644000175000017500000000147612665157707024355 0ustar bmwbmw00000000000000-----BEGIN CERTIFICATE----- MIICNzCCAeGgAwIBAgIJALizm9Y3q620MA0GCSqGSIb3DQEBCwUAMHcxCzAJBgNV BAYTAlVTMREwDwYDVQQIDAhNaWNoaWdhbjESMBAGA1UEBwwJQW5uIEFyYm9yMSsw KQYDVQQKDCJVbml2ZXJzaXR5IG9mIE1pY2hpZ2FuIGFuZCB0aGUgRUZGMRQwEgYD VQQDDAtleGFtcGxlLmNvbTAeFw0xNTA1MDkwMDI0NTJaFw0xNjA1MDgwMDI0NTJa MHcxCzAJBgNVBAYTAlVTMREwDwYDVQQIDAhNaWNoaWdhbjESMBAGA1UEBwwJQW5u IEFyYm9yMSswKQYDVQQKDCJVbml2ZXJzaXR5IG9mIE1pY2hpZ2FuIGFuZCB0aGUg RUZGMRQwEgYDVQQDDAtleGFtcGxlLmNvbTBcMA0GCSqGSIb3DQEBAQUAA0sAMEgC QQD0thFxUTc2v6qV55wRxfwnBUOeN4bVfu5ywJqy65kzR7T1yZi5TPEiQyM7/3Hg BVy9ddFc8RX4vNZaR+ROXNEzAgMBAAGjUDBOMB0GA1UdDgQWBBRJieHEVSHKmBk0 mTExx1erzlylCjAfBgNVHSMEGDAWgBRJieHEVSHKmBk0mTExx1erzlylCjAMBgNV HRMEBTADAQH/MA0GCSqGSIb3DQEBCwUAA0EABT/nlpqOaanFSLZmWIrKv0zt63k4 bmWNMA8fYT45KYpLomsW8qXdpC82IlVKfNk7fW0UYT3HOeDSJRcycxNCTQ== -----END CERTIFICATE----- letsencrypt-0.4.1/letsencrypt/tests/testdata/rsa256_key.pem0000644000175000017500000000045212665157707023431 0ustar bmwbmw00000000000000-----BEGIN RSA PRIVATE KEY----- MIGrAgEAAiEAm2Fylv+Uz7trgTW8EBHP3FQSMeZs2GNQ6VRo1sIVJEkCAwEAAQIh AJT0BA/xD01dFCAXzSNyj9nfSZa3NpqzJZZn/eOm7vghAhEAzUVNZn4lLLBD1R6N E8TKNQIRAMHHyn3O5JeY36lwKwkUlEUCEAliRauN0L0+QZuYjfJ9aJECEGx4dru3 rTPCyighdqWNlHUCEQCiLjlwSRtWgmMBudCkVjzt -----END RSA PRIVATE KEY----- letsencrypt-0.4.1/letsencrypt/tests/testdata/csr-6sans.pem0000644000175000017500000000124412665157707023356 0ustar bmwbmw00000000000000-----BEGIN CERTIFICATE REQUEST----- MIIBuzCCAWUCAQAweTELMAkGA1UEBhMCVVMxETAPBgNVBAgTCE1pY2hpZ2FuMRIw EAYDVQQHEwlBbm4gQXJib3IxDDAKBgNVBAoTA0VGRjEfMB0GA1UECxMWVW5pdmVy c2l0eSBvZiBNaWNoaWdhbjEUMBIGA1UEAxMLZXhhbXBsZS5jb20wXDANBgkqhkiG 9w0BAQEFAANLADBIAkEA9LYRcVE3Nr+qleecEcX8JwVDnjeG1X7ucsCasuuZM0e0 9cmYuUzxIkMjO/9x4AVcvXXRXPEV+LzWWkfkTlzRMwIDAQABoIGGMIGDBgkqhkiG 9w0BCQ4xdjB0MHIGA1UdEQRrMGmCC2V4YW1wbGUuY29tggtleGFtcGxlLm9yZ4IL ZXhhbXBsZS5uZXSCDGV4YW1wbGUuaW5mb4IVc3ViZG9tYWluLmV4YW1wbGUuY29t ghtvdGhlci5zdWJkb21haW4uZXhhbXBsZS5jb20wDQYJKoZIhvcNAQELBQADQQBd k4BE5qvEvkYoZM/2++Xd9RrQ6wsdj0QiJQCozfsI4lQx6ZJnbtNc7HpDrX4W6XIv IvzVBz/nD11drfz/RNuX -----END CERTIFICATE REQUEST----- letsencrypt-0.4.1/letsencrypt/tests/testdata/cert.pem0000644000175000017500000000130512665157707022472 0ustar bmwbmw00000000000000-----BEGIN CERTIFICATE----- MIIB3jCCAYigAwIBAgICBTkwDQYJKoZIhvcNAQELBQAwdzELMAkGA1UEBhMCVVMx ETAPBgNVBAgMCE1pY2hpZ2FuMRIwEAYDVQQHDAlBbm4gQXJib3IxKzApBgNVBAoM IlVuaXZlcnNpdHkgb2YgTWljaGlnYW4gYW5kIHRoZSBFRkYxFDASBgNVBAMMC2V4 YW1wbGUuY29tMB4XDTE0MTIxMTIyMzQ0NVoXDTE0MTIxODIyMzQ0NVowdzELMAkG A1UEBhMCVVMxETAPBgNVBAgMCE1pY2hpZ2FuMRIwEAYDVQQHDAlBbm4gQXJib3Ix KzApBgNVBAoMIlVuaXZlcnNpdHkgb2YgTWljaGlnYW4gYW5kIHRoZSBFRkYxFDAS BgNVBAMMC2V4YW1wbGUuY29tMFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBAKx1c7RR 7R/drnBSQ/zfx1vQLHUbFLh1AQQQ5R8DZUXd36efNK79vukFhN9HFoHZiUvOjm0c +pVE6K+EdE/twuUCAwEAATANBgkqhkiG9w0BAQsFAANBAC24z0IdwIVKSlntksll vr6zJepBH5fMndfk3XJp10jT6VE+14KNtjh02a56GoraAvJAT5/H67E8GvJ/ocNn B/o= -----END CERTIFICATE----- letsencrypt-0.4.1/letsencrypt/tests/testdata/cli.ini0000644000175000017500000000003112665157707022275 0ustar bmwbmw00000000000000agree-dev-preview = True letsencrypt-0.4.1/letsencrypt/tests/testdata/csr.der0000644000175000017500000000054112665157707022316 0ustar bmwbmw000000000000000‚]0‚0y1 0 UUS10U Michigan10U Ann Arbor1 0 U EFF10U University of Michigan10U example.com0\0  *†H†÷ K0HA¬us´QíÝ®pRCüßÇ[Ð,u¸uåeEÝß§Ÿ4®ý¾é„ßGÙ‰KÎŽmú•D评tOíÂå )0' *†H†÷  100U0 ‚ example.com0  *†H†÷  ArGüî´/Z1ÁŽg¼ýˆ„GY,ë÷Û;9û„¼,msÀ€íPÄL ù¸ƒmŸÕÁS}[¡Â K­Ùß0(letsencrypt-0.4.1/letsencrypt/tests/testdata/cert.b64jose0000644000175000017500000000120312665157707023162 0ustar bmwbmw00000000000000MIIB3jCCAYigAwIBAgICBTkwDQYJKoZIhvcNAQELBQAwdzELMAkGA1UEBhMCVVMxETAPBgNVBAgMCE1pY2hpZ2FuMRIwEAYDVQQHDAlBbm4gQXJib3IxKzApBgNVBAoMIlVuaXZlcnNpdHkgb2YgTWljaGlnYW4gYW5kIHRoZSBFRkYxFDASBgNVBAMMC2V4YW1wbGUuY29tMB4XDTE0MTIxMTIyMzQ0NVoXDTE0MTIxODIyMzQ0NVowdzELMAkGA1UEBhMCVVMxETAPBgNVBAgMCE1pY2hpZ2FuMRIwEAYDVQQHDAlBbm4gQXJib3IxKzApBgNVBAoMIlVuaXZlcnNpdHkgb2YgTWljaGlnYW4gYW5kIHRoZSBFRkYxFDASBgNVBAMMC2V4YW1wbGUuY29tMFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBAKx1c7RR7R_drnBSQ_zfx1vQLHUbFLh1AQQQ5R8DZUXd36efNK79vukFhN9HFoHZiUvOjm0c-pVE6K-EdE_twuUCAwEAATANBgkqhkiG9w0BAQsFAANBAC24z0IdwIVKSlntksllvr6zJepBH5fMndfk3XJp10jT6VE-14KNtjh02a56GoraAvJAT5_H67E8GvJ_ocNnB_oletsencrypt-0.4.1/letsencrypt/tests/testdata/cert.der0000644000175000017500000000057112665157707022467 0ustar bmwbmw000000000000000‚u0‚  ­Â–oKÛ1\0  *†H†÷  010U example.com0 150622040037Z 150722040037Z010U example.com0\0  *†H†÷ K0HA¬us´QíÝ®pRCüßÇ[Ð,u¸uåeEÝß§Ÿ4®ý¾é„ßGÙ‰KÎŽmú•D评tOíÂå£P0N0U&ÏÈíK×”²ä%X$ÀtÕ—Š0U#0€&ÏÈíK×”²ä%X$ÀtÕ—Š0 U0ÿ0  *†H†÷  A ‹ÿ€žL¼&°– «v dqÒH¥3öGäßv^Íá^ÑM¸c/É}n\;ËÍ£ÐØ'tf£v¥û:¶letsencrypt-0.4.1/letsencrypt/tests/testdata/sample-renewal-ancient.conf0000755000175000017500000000360212665157707026241 0ustar bmwbmw00000000000000cert = MAGICDIR/live/sample-renewal/cert.pem privkey = MAGICDIR/live/sample-renewal/privkey.pem chain = MAGICDIR/live/sample-renewal/chain.pem fullchain = MAGICDIR/live/sample-renewal/fullchain.pem renew_before_expiry = 1 year # Options and defaults used in the renewal process [renewalparams] no_self_upgrade = False apache_enmod = a2enmod no_verify_ssl = False ifaces = None apache_dismod = a2dismod register_unsafely_without_email = False apache_handle_modules = True uir = None installer = none nginx_ctl = nginx config_dir = MAGICDIR text_mode = False func = staging = True prepare = False work_dir = /var/lib/letsencrypt tos = False init = False http01_port = 80 duplicate = False noninteractive_mode = True key_path = None nginx = False nginx_server_root = /etc/nginx fullchain_path = /home/ubuntu/letsencrypt/chain.pem email = None csr = None agree_dev_preview = None redirect = None verb = certonly verbose_count = -3 config_file = None renew_by_default = False hsts = False apache_handle_sites = True authenticator = webroot domains = isnot.org, rsa_key_size = 2048 apache_challenge_location = /etc/apache2 checkpoints = 1 manual_test_mode = False apache = False cert_path = /home/ubuntu/letsencrypt/cert.pem webroot_path = /var/www/ reinstall = False expand = False strict_permissions = False apache_server_root = /etc/apache2 account = None dry_run = False manual_public_ip_logging_ok = False chain_path = /home/ubuntu/letsencrypt/chain.pem break_my_certs = False standalone = True manual = False server = https://acme-staging.api.letsencrypt.org/directory standalone_supported_challenges = "tls-sni-01,http-01" webroot = True os_packages_only = False apache_init_script = None user_agent = None apache_le_vhost_ext = -le-ssl.conf debug = False tls_sni_01_port = 443 logs_dir = /var/log/letsencrypt apache_vhost_root = /etc/apache2/sites-available configurator = None letsencrypt-0.4.1/letsencrypt/tests/testdata/rsa512_key_2.pem0000644000175000017500000000076112665157707023650 0ustar bmwbmw00000000000000-----BEGIN RSA PRIVATE KEY----- MIIBOwIBAAJBAPS2EXFRNza/qpXnnBHF/CcFQ543htV+7nLAmrLrmTNHtPXJmLlM 8SJDIzv/ceAFXL110VzxFfi81lpH5E5c0TMCAwEAAQJBALmppYQ/JVARjWBcsEm/ 1/bXBJ127YLv4gQIY5baL4r6IdEE33OXMTTmD9wf+ajuq1eaH0htHkwhOvREu0sz bskCIQD/Cg+xhEVLcwK3pFp3afPIhj1IPFiL3Uy/nqyMZ6O/RQIhAPWiDBofp7Cp J4dGZs+hkRySq/IOeeRJlNK1Pq64nToXAiBZ7+te1100YSd5KT051SRB94zO13EG SZESFduVW8rz3QIgK+tLiqg6TYYRQUi/PUTAM4GuKNuZw828RGiPyqHLywUCIQCd pkZrNphL/y0D7HSbPIfZzD90M2V8tUjlK0BTqk1bHA== -----END RSA PRIVATE KEY----- letsencrypt-0.4.1/letsencrypt/tests/testdata/csr-san.der0000644000175000017500000000056212665157707023100 0ustar bmwbmw000000000000000‚n0‚0y1 0 UUS10U Michigan10U Ann Arbor1 0 U EFF10U University of Michigan10U example.com0\0  *†H†÷ K0HA¬us´QíÝ®pRCüßÇ[Ð,u¸uåeEÝß§Ÿ4®ý¾é„ßGÙ‰KÎŽmú•D评tOíÂå :08 *†H†÷  1+0)0'U 0‚ example.com‚www.example.com0  *†H†÷  Ad`LðkFÎèœX-s¾¦9êÕÍïÑ››oe²¶d^c¥õ7‡,ßÀýU*0ùS»bÖ«óK´T&“iä3Ôletsencrypt-0.4.1/letsencrypt/tests/testdata/csr-san.pem0000644000175000017500000000107612665157707023110 0ustar bmwbmw00000000000000-----BEGIN CERTIFICATE REQUEST----- MIIBbjCCARgCAQAweTELMAkGA1UEBhMCVVMxETAPBgNVBAgMCE1pY2hpZ2FuMRIw EAYDVQQHDAlBbm4gQXJib3IxDDAKBgNVBAoMA0VGRjEfMB0GA1UECwwWVW5pdmVy c2l0eSBvZiBNaWNoaWdhbjEUMBIGA1UEAwwLZXhhbXBsZS5jb20wXDANBgkqhkiG 9w0BAQEFAANLADBIAkEArHVztFHtH92ucFJD/N/HW9AsdRsUuHUBBBDlHwNlRd3f p580rv2+6QWE30cWgdmJS86ObRz6lUTor4R0T+3C5QIDAQABoDowOAYJKoZIhvcN AQkOMSswKTAnBgNVHREEIDAeggtleGFtcGxlLmNvbYIPd3d3LmV4YW1wbGUuY29t MA0GCSqGSIb3DQEBCwUAA0EAZGBM8J1rRs7onFgtc76mOeoT1c3v0ZsEmxQfb2Wy tmReY6X1N4cs38D9VSow+VMRu2LWkKvzS7RUFSaTaeQz1A== -----END CERTIFICATE REQUEST----- letsencrypt-0.4.1/letsencrypt/tests/testdata/archive/0000755000175000017500000000000012665157717022455 5ustar bmwbmw00000000000000letsencrypt-0.4.1/letsencrypt/tests/testdata/archive/sample-renewal/0000755000175000017500000000000012665157717025371 5ustar bmwbmw00000000000000letsencrypt-0.4.1/letsencrypt/tests/testdata/archive/sample-renewal/fullchain1.pem0000644000175000017500000000545312665157707030130 0ustar bmwbmw00000000000000-----BEGIN CERTIFICATE----- MIIE1DCCA7ygAwIBAgITAPoz/CBluNQV/Eh9F+CS6dSxEDANBgkqhkiG9w0BAQsF ADAfMR0wGwYDVQQDDBRoYXBweSBoYWNrZXIgZmFrZSBDQTAeFw0xNjAyMDIyMzQ5 MDBaFw0xNjA1MDIyMzQ5MDBaMBQxEjAQBgNVBAMTCWlzbm90Lm9yZzCCASIwDQYJ KoZIhvcNAQEBBQADggEPADCCAQoCggEBALyudqLKcIdWZ5VaK1fuhlEDbZtvs2E+ slm4dmSS1nFve7MdlZ69K0gdtnhkiPQ0wGQTligeDZ8fY8iL87GZO0tp5f7S+QJN NYCiYw6j4qp5JBy/zG22kJz1Quu7/vXMYLzLvK6x6YixiWAWyqqvlUVBLS1r4W3h A5Z+F1EIsXeyz7TJe3lAzIWAAxpfH9OviIz2rEDotuCdU771USLLNSw4qJojNlTx UpZG6lGFs8KGb8tqROXknaMKE4PvN3SITixSUTFbktt1Wz60moWbNdLMKvgkzuUP r4viO2P4SO5slNAY0ZeEssPpVAelN3EvrAcEZtoKmG5fnQDVo8uVag0CAwEAAaOC AhIwggIOMA4GA1UdDwEB/wQEAwIFoDAdBgNVHSUEFjAUBggrBgEFBQcDAQYIKwYB BQUHAwIwDAYDVR0TAQH/BAIwADAdBgNVHQ4EFgQUqhI4u6aaPrcYQnmypxV8Tap8 L54wHwYDVR0jBBgwFoAU+3hPEvlgFYMsnxd/NBmzLjbqQYkweAYIKwYBBQUHAQEE bDBqMDMGCCsGAQUFBzABhidodHRwOi8vb2NzcC5zdGFnaW5nLXgxLmxldHNlbmNy eXB0Lm9yZy8wMwYIKwYBBQUHMAKGJ2h0dHA6Ly9jZXJ0LnN0YWdpbmcteDEubGV0 c2VuY3J5cHQub3JnLzAUBgNVHREEDTALgglpc25vdC5vcmcwgf4GA1UdIASB9jCB 8zAIBgZngQwBAgEwgeYGCysGAQQBgt8TAQEBMIHWMCYGCCsGAQUFBwIBFhpodHRw Oi8vY3BzLmxldHNlbmNyeXB0Lm9yZzCBqwYIKwYBBQUHAgIwgZ4MgZtUaGlzIENl cnRpZmljYXRlIG1heSBvbmx5IGJlIHJlbGllZCB1cG9uIGJ5IFJlbHlpbmcgUGFy dGllcyBhbmQgb25seSBpbiBhY2NvcmRhbmNlIHdpdGggdGhlIENlcnRpZmljYXRl IFBvbGljeSBmb3VuZCBhdCBodHRwczovL2xldHNlbmNyeXB0Lm9yZy9yZXBvc2l0 b3J5LzANBgkqhkiG9w0BAQsFAAOCAQEAbAhX6FfQwELayneY4l5RvYSdw/Jj5CRy KzrM7ISld7x9YPpxX6Pmht/YyMhLWrtxvFUR2+RNhSIYB8IjQEjmKjvR7UNeiUve jzPEAuTg/9m3i0FJpPHc2aKGzlLFQCMm5/RrvnXI6ljIcyhocLvMiN46iexcExI2 Ese3w8GoH6wARYKxU/QBexfoXQLgtAbYzNRE6EgKWtB+txV+7+d2MgbhCEit5VwU +ydT8inp9URsA7iKM03hDdGOBysddkrm1/yEhVy/Oo6bT9WMAUHVvz61hHekWcSf rAQ6BayubvWOUx06eTowXr1gln/rl+WXOxcsJeag127NuhmHOCXZxQ== -----END CERTIFICATE----- -----BEGIN CERTIFICATE----- MIIDETCCAfmgAwIBAgIJAJzxkS6o1QkIMA0GCSqGSIb3DQEBCwUAMB8xHTAbBgNV BAMMFGhhcHB5IGhhY2tlciBmYWtlIENBMB4XDTE1MDQwNzIzNTAzOFoXDTI1MDQw NDIzNTAzOFowHzEdMBsGA1UEAwwUaGFwcHkgaGFja2VyIGZha2UgQ0EwggEiMA0G CSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDCCkd5mgXFErJ3F2M0E9dw+Ta/md5i 8TDId01HberAApqmydG7UZYF3zLTSzNjlNSOmtybvrSGUnZ9r9tSQcL8VM6WUOM8 tnIpiIjEA2QkBycMwvRmZ/B2ltPdYs/R9BqNwO1g18GDZrHSzUYtNKNeFI6Glamj 7GK2Vr0SmiEamlNIR5ktAFsEErzf/d4jCF7sosMsJpMCm1p58QkP4LHLShVLXDa8 BMfVoI+ipYcA08iNUFkgW8VWDclIDxcysa0psDDtMjX3+4aPkE/cefmP+1xOfUuD HOGV8XFynsP4EpTfVOZr0/g9gYQ7ZArqXX7GTQkFqduwPm/w5qxSPTarAgMBAAGj UDBOMB0GA1UdDgQWBBT7eE8S+WAVgyyfF380GbMuNupBiTAfBgNVHSMEGDAWgBT7 eE8S+WAVgyyfF380GbMuNupBiTAMBgNVHRMEBTADAQH/MA0GCSqGSIb3DQEBCwUA A4IBAQAd9Da+Zv+TjMv7NTAmliqnWHY6d3UxEZN3hFEJ58IQVHbBZVZdW7zhRktB vR05Kweac0HJeK91TKmzvXl21IXLvh0gcNLU/uweD3no/snfdB4OoFompljThmgl zBqiqWoKBJQrLCA8w5UB+ReomRYd/EYXF/6TAfzm6hr//Xt5mPiUHPdvYt75lMAo vRxLSbF8TSQ6b7BYxISWjPgFASNNqJNHEItWsmQMtAjjwzb9cs01XH9pChVAWn9L oeMKa+SlHSYrWG93+EcrIH/dGU76uNOiaDzBSKvaehG53h25MHuO1anNICJvZovW rFo4Uv1EnkKJm3vJFe50eJGhEKlx -----END CERTIFICATE----- letsencrypt-0.4.1/letsencrypt/tests/testdata/archive/sample-renewal/privkey1.pem0000644000175000017500000000325012665157707027645 0ustar bmwbmw00000000000000-----BEGIN PRIVATE KEY----- MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQC8rnaiynCHVmeV WitX7oZRA22bb7NhPrJZuHZkktZxb3uzHZWevStIHbZ4ZIj0NMBkE5YoHg2fH2PI i/OxmTtLaeX+0vkCTTWAomMOo+KqeSQcv8xttpCc9ULru/71zGC8y7yusemIsYlg Fsqqr5VFQS0ta+Ft4QOWfhdRCLF3ss+0yXt5QMyFgAMaXx/Tr4iM9qxA6LbgnVO+ 9VEiyzUsOKiaIzZU8VKWRupRhbPChm/LakTl5J2jChOD7zd0iE4sUlExW5LbdVs+ tJqFmzXSzCr4JM7lD6+L4jtj+EjubJTQGNGXhLLD6VQHpTdxL6wHBGbaCphuX50A 1aPLlWoNAgMBAAECggEAfKKWFWS6PnwSAnNErFoQeZVVItb/XB5JO8EA2+CvLNFi mefR/MCixYlzDkYCvaXW7ISPrMJlZxYaGNBx0oAQzfkPB2wfNqj/zY/29SXGxast 8puzk0mEb1oHsaZGfeFaiXvfkFpPlI8J2uJTT7qaVNv/1sArciSv9QonpsyiRhlB yqT49juNVoR1tJHyXzkkRfHKTG8OlJd4kuFOl3fM9dTFPQ/ft0kTNAQ/B4SFvSwF RJsbLbsbFGsUdV9ekE6UX6oWD/Ah707rvgtCyS0Bc+0O3t2EKwmm3RXPRUMHCVxE bKdTxRB4etbjMVXMuVhB8Y4GbfrtMCy+qxZQ6znCAQKBgQDr7bcYAZVZp/nBMVB+ lBO9w73J6lnEWm6bZ9728KlGAKETaRhxZQSi6TN6MWwNwnk6rinyz4uVwVr9ZRCs WkB1TbvW0JNcWdr3YClwsKXAt8X22bjGe0LagDJHG6r1TPS+MdovOS2M6IMaxlbT rzFhSJ8ojLX3tqnOsmc7YAFLjQKBgQDMu8E9hoJt82lQzOGrjHmGzGEu2GLx9WKO e4nkj335kX6fIhMMqSXBFbTJZwXoYvk5J8ZnaARbYG0m5nxDCwRjX5HWa8q0B2Po ta53w01sKKznzlPjUhsdhEthun7MCFfLZpgvcZ9xVzOXo3/Zfn2+RrsPSjrVDqBy hj+k5mW4gQKBgHFWKf3LTO7cBdvsD8ou4mjn7nVgMi1kb/wR4wdnxzmMtdR4STi4 GYkVVBhgQ5M8mDY7UoWFdH3FfCt8cI0Lcimn5ROl8RSNSeZKeL3c7lNtNRmHr/8R WaVTrlOAlBjxFiWEF1dWNW6ah9jF7RIV+DfOxj6ZkhTk2CAmjfb1AMpFAoGABf96 KdNG/vGipDtcYSo8ZTaXoke0nmISARqdb5TEnAsnKoJVDInoEUARi9T411YO9x2z MlRZzFOG3xzhhxVLi53BKAcAaUXOJ4MrGVcfbYvDhQcGbiJ5qOO3UaWlEVUtPUhE LR+nDCsB1+9yT2zlQi3QTSJflt5W1QQZ2TrmwAECgYEAvQ7+sTcHs1K9yKj7koEu A19FbMA0IwvrVRcV/VqmlsoW6e6wW2YND+GtaDbKdD0aBPivqLJwpNFrsRA+W0iB vzmML6sKhhL+j7tjSgq+iQdBkKz0j9PyReuhe9CRnljMmyun+4qKEk0KUvxBrjPY Skn+ML18qyUoEPnmbpfHxCs= -----END PRIVATE KEY----- letsencrypt-0.4.1/letsencrypt/tests/testdata/archive/sample-renewal/chain1.pem0000644000175000017500000000214312665157707027236 0ustar bmwbmw00000000000000-----BEGIN CERTIFICATE----- MIIDETCCAfmgAwIBAgIJAJzxkS6o1QkIMA0GCSqGSIb3DQEBCwUAMB8xHTAbBgNV BAMMFGhhcHB5IGhhY2tlciBmYWtlIENBMB4XDTE1MDQwNzIzNTAzOFoXDTI1MDQw NDIzNTAzOFowHzEdMBsGA1UEAwwUaGFwcHkgaGFja2VyIGZha2UgQ0EwggEiMA0G CSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDCCkd5mgXFErJ3F2M0E9dw+Ta/md5i 8TDId01HberAApqmydG7UZYF3zLTSzNjlNSOmtybvrSGUnZ9r9tSQcL8VM6WUOM8 tnIpiIjEA2QkBycMwvRmZ/B2ltPdYs/R9BqNwO1g18GDZrHSzUYtNKNeFI6Glamj 7GK2Vr0SmiEamlNIR5ktAFsEErzf/d4jCF7sosMsJpMCm1p58QkP4LHLShVLXDa8 BMfVoI+ipYcA08iNUFkgW8VWDclIDxcysa0psDDtMjX3+4aPkE/cefmP+1xOfUuD HOGV8XFynsP4EpTfVOZr0/g9gYQ7ZArqXX7GTQkFqduwPm/w5qxSPTarAgMBAAGj UDBOMB0GA1UdDgQWBBT7eE8S+WAVgyyfF380GbMuNupBiTAfBgNVHSMEGDAWgBT7 eE8S+WAVgyyfF380GbMuNupBiTAMBgNVHRMEBTADAQH/MA0GCSqGSIb3DQEBCwUA A4IBAQAd9Da+Zv+TjMv7NTAmliqnWHY6d3UxEZN3hFEJ58IQVHbBZVZdW7zhRktB vR05Kweac0HJeK91TKmzvXl21IXLvh0gcNLU/uweD3no/snfdB4OoFompljThmgl zBqiqWoKBJQrLCA8w5UB+ReomRYd/EYXF/6TAfzm6hr//Xt5mPiUHPdvYt75lMAo vRxLSbF8TSQ6b7BYxISWjPgFASNNqJNHEItWsmQMtAjjwzb9cs01XH9pChVAWn9L oeMKa+SlHSYrWG93+EcrIH/dGU76uNOiaDzBSKvaehG53h25MHuO1anNICJvZovW rFo4Uv1EnkKJm3vJFe50eJGhEKlx -----END CERTIFICATE----- letsencrypt-0.4.1/letsencrypt/tests/testdata/archive/sample-renewal/cert1.pem0000644000175000017500000000331012665157707027106 0ustar bmwbmw00000000000000-----BEGIN CERTIFICATE----- MIIE1DCCA7ygAwIBAgITAPoz/CBluNQV/Eh9F+CS6dSxEDANBgkqhkiG9w0BAQsF ADAfMR0wGwYDVQQDDBRoYXBweSBoYWNrZXIgZmFrZSBDQTAeFw0xNjAyMDIyMzQ5 MDBaFw0xNjA1MDIyMzQ5MDBaMBQxEjAQBgNVBAMTCWlzbm90Lm9yZzCCASIwDQYJ KoZIhvcNAQEBBQADggEPADCCAQoCggEBALyudqLKcIdWZ5VaK1fuhlEDbZtvs2E+ slm4dmSS1nFve7MdlZ69K0gdtnhkiPQ0wGQTligeDZ8fY8iL87GZO0tp5f7S+QJN NYCiYw6j4qp5JBy/zG22kJz1Quu7/vXMYLzLvK6x6YixiWAWyqqvlUVBLS1r4W3h A5Z+F1EIsXeyz7TJe3lAzIWAAxpfH9OviIz2rEDotuCdU771USLLNSw4qJojNlTx UpZG6lGFs8KGb8tqROXknaMKE4PvN3SITixSUTFbktt1Wz60moWbNdLMKvgkzuUP r4viO2P4SO5slNAY0ZeEssPpVAelN3EvrAcEZtoKmG5fnQDVo8uVag0CAwEAAaOC AhIwggIOMA4GA1UdDwEB/wQEAwIFoDAdBgNVHSUEFjAUBggrBgEFBQcDAQYIKwYB BQUHAwIwDAYDVR0TAQH/BAIwADAdBgNVHQ4EFgQUqhI4u6aaPrcYQnmypxV8Tap8 L54wHwYDVR0jBBgwFoAU+3hPEvlgFYMsnxd/NBmzLjbqQYkweAYIKwYBBQUHAQEE bDBqMDMGCCsGAQUFBzABhidodHRwOi8vb2NzcC5zdGFnaW5nLXgxLmxldHNlbmNy eXB0Lm9yZy8wMwYIKwYBBQUHMAKGJ2h0dHA6Ly9jZXJ0LnN0YWdpbmcteDEubGV0 c2VuY3J5cHQub3JnLzAUBgNVHREEDTALgglpc25vdC5vcmcwgf4GA1UdIASB9jCB 8zAIBgZngQwBAgEwgeYGCysGAQQBgt8TAQEBMIHWMCYGCCsGAQUFBwIBFhpodHRw Oi8vY3BzLmxldHNlbmNyeXB0Lm9yZzCBqwYIKwYBBQUHAgIwgZ4MgZtUaGlzIENl cnRpZmljYXRlIG1heSBvbmx5IGJlIHJlbGllZCB1cG9uIGJ5IFJlbHlpbmcgUGFy dGllcyBhbmQgb25seSBpbiBhY2NvcmRhbmNlIHdpdGggdGhlIENlcnRpZmljYXRl IFBvbGljeSBmb3VuZCBhdCBodHRwczovL2xldHNlbmNyeXB0Lm9yZy9yZXBvc2l0 b3J5LzANBgkqhkiG9w0BAQsFAAOCAQEAbAhX6FfQwELayneY4l5RvYSdw/Jj5CRy KzrM7ISld7x9YPpxX6Pmht/YyMhLWrtxvFUR2+RNhSIYB8IjQEjmKjvR7UNeiUve jzPEAuTg/9m3i0FJpPHc2aKGzlLFQCMm5/RrvnXI6ljIcyhocLvMiN46iexcExI2 Ese3w8GoH6wARYKxU/QBexfoXQLgtAbYzNRE6EgKWtB+txV+7+d2MgbhCEit5VwU +ydT8inp9URsA7iKM03hDdGOBysddkrm1/yEhVy/Oo6bT9WMAUHVvz61hHekWcSf rAQ6BayubvWOUx06eTowXr1gln/rl+WXOxcsJeag127NuhmHOCXZxQ== -----END CERTIFICATE----- letsencrypt-0.4.1/letsencrypt/tests/storage_test.py0000644000175000017500000007763512665157707022321 0ustar bmwbmw00000000000000"""Tests for letsencrypt.storage.""" import datetime import os import shutil import tempfile import unittest import configobj import mock import pytz from letsencrypt import configuration from letsencrypt import errors from letsencrypt.storage import ALL_FOUR from letsencrypt.tests import test_util CERT = test_util.load_cert('cert.pem') def unlink_all(rc_object): """Unlink all four items associated with this RenewableCert.""" for kind in ALL_FOUR: os.unlink(getattr(rc_object, kind)) def fill_with_sample_data(rc_object): """Put dummy data into all four files of this RenewableCert.""" for kind in ALL_FOUR: with open(getattr(rc_object, kind), "w") as f: f.write(kind) class BaseRenewableCertTest(unittest.TestCase): """Base class for setting up Renewable Cert tests. .. note:: It may be required to write out self.config for your test. Check :class:`.cli_test.DuplicateCertTest` for an example. """ def setUp(self): from letsencrypt import storage self.tempdir = tempfile.mkdtemp() self.cli_config = configuration.RenewerConfiguration( namespace=mock.MagicMock( config_dir=self.tempdir, work_dir=self.tempdir, logs_dir=self.tempdir, ) ) # TODO: maybe provide RenewerConfiguration.make_dirs? # TODO: main() should create those dirs, c.f. #902 os.makedirs(os.path.join(self.tempdir, "live", "example.org")) os.makedirs(os.path.join(self.tempdir, "archive", "example.org")) os.makedirs(os.path.join(self.tempdir, "renewal")) config = configobj.ConfigObj() for kind in ALL_FOUR: config[kind] = os.path.join(self.tempdir, "live", "example.org", kind + ".pem") config.filename = os.path.join(self.tempdir, "renewal", "example.org.conf") config.write() self.config = config # We also create a file that isn't a renewal config in the same # location to test that logic that reads in all-and-only renewal # configs will ignore it and NOT attempt to parse it. junk = open(os.path.join(self.tempdir, "renewal", "IGNORE.THIS"), "w") junk.write("This file should be ignored!") junk.close() self.defaults = configobj.ConfigObj() with mock.patch("letsencrypt.storage.RenewableCert._check_symlinks") as check: check.return_value = True self.test_rc = storage.RenewableCert(config.filename, self.cli_config) def tearDown(self): shutil.rmtree(self.tempdir) def _write_out_ex_kinds(self): for kind in ALL_FOUR: where = getattr(self.test_rc, kind) os.symlink(os.path.join("..", "..", "archive", "example.org", "{0}12.pem".format(kind)), where) with open(where, "w") as f: f.write(kind) os.unlink(where) os.symlink(os.path.join("..", "..", "archive", "example.org", "{0}11.pem".format(kind)), where) with open(where, "w") as f: f.write(kind) class RenewableCertTests(BaseRenewableCertTest): # pylint: disable=too-many-public-methods """Tests for letsencrypt.storage.""" def test_initialization(self): self.assertEqual(self.test_rc.lineagename, "example.org") for kind in ALL_FOUR: self.assertEqual( getattr(self.test_rc, kind), os.path.join( self.tempdir, "live", "example.org", kind + ".pem")) def test_renewal_bad_config(self): """Test that the RenewableCert constructor will complain if the renewal configuration file doesn't end in ".conf" """ from letsencrypt import storage broken = os.path.join(self.tempdir, "broken.conf") with open(broken, "w") as f: f.write("[No closing bracket for you!") self.assertRaises(errors.CertStorageError, storage.RenewableCert, broken, self.cli_config) os.unlink(broken) self.assertRaises(errors.CertStorageError, storage.RenewableCert, "fun", self.cli_config) def test_renewal_incomplete_config(self): """Test that the RenewableCert constructor will complain if the renewal configuration file is missing a required file element.""" from letsencrypt import storage config = configobj.ConfigObj() config["cert"] = "imaginary_cert.pem" # Here the required privkey is missing. config["chain"] = "imaginary_chain.pem" config["fullchain"] = "imaginary_fullchain.pem" config.filename = os.path.join(self.tempdir, "imaginary_config.conf") config.write() self.assertRaises(errors.CertStorageError, storage.RenewableCert, config.filename, self.cli_config) def test_consistent(self): # pylint: disable=too-many-statements,protected-access oldcert = self.test_rc.cert self.test_rc.cert = "relative/path" # Absolute path for item requirement self.assertFalse(self.test_rc._consistent()) self.test_rc.cert = oldcert # Items must exist requirement self.assertFalse(self.test_rc._consistent()) # Items must be symlinks requirements fill_with_sample_data(self.test_rc) self.assertFalse(self.test_rc._consistent()) unlink_all(self.test_rc) # Items must point to desired place if they are relative for kind in ALL_FOUR: os.symlink(os.path.join("..", kind + "17.pem"), getattr(self.test_rc, kind)) self.assertFalse(self.test_rc._consistent()) unlink_all(self.test_rc) # Items must point to desired place if they are absolute for kind in ALL_FOUR: os.symlink(os.path.join(self.tempdir, kind + "17.pem"), getattr(self.test_rc, kind)) self.assertFalse(self.test_rc._consistent()) unlink_all(self.test_rc) # Items must point to things that exist for kind in ALL_FOUR: os.symlink(os.path.join("..", "..", "archive", "example.org", kind + "17.pem"), getattr(self.test_rc, kind)) self.assertFalse(self.test_rc._consistent()) # This version should work fill_with_sample_data(self.test_rc) self.assertTrue(self.test_rc._consistent()) # Items must point to things that follow the naming convention os.unlink(self.test_rc.fullchain) os.symlink(os.path.join("..", "..", "archive", "example.org", "fullchain_17.pem"), self.test_rc.fullchain) with open(self.test_rc.fullchain, "w") as f: f.write("wrongly-named fullchain") self.assertFalse(self.test_rc._consistent()) def test_current_target(self): # Relative path logic os.symlink(os.path.join("..", "..", "archive", "example.org", "cert17.pem"), self.test_rc.cert) with open(self.test_rc.cert, "w") as f: f.write("cert") self.assertTrue(os.path.samefile(self.test_rc.current_target("cert"), os.path.join(self.tempdir, "archive", "example.org", "cert17.pem"))) # Absolute path logic os.unlink(self.test_rc.cert) os.symlink(os.path.join(self.tempdir, "archive", "example.org", "cert17.pem"), self.test_rc.cert) with open(self.test_rc.cert, "w") as f: f.write("cert") self.assertTrue(os.path.samefile(self.test_rc.current_target("cert"), os.path.join(self.tempdir, "archive", "example.org", "cert17.pem"))) def test_current_version(self): for ver in (1, 5, 10, 20): os.symlink(os.path.join("..", "..", "archive", "example.org", "cert{0}.pem".format(ver)), self.test_rc.cert) with open(self.test_rc.cert, "w") as f: f.write("cert") os.unlink(self.test_rc.cert) os.symlink(os.path.join("..", "..", "archive", "example.org", "cert10.pem"), self.test_rc.cert) self.assertEqual(self.test_rc.current_version("cert"), 10) def test_no_current_version(self): self.assertEqual(self.test_rc.current_version("cert"), None) def test_latest_and_next_versions(self): for ver in xrange(1, 6): for kind in ALL_FOUR: where = getattr(self.test_rc, kind) if os.path.islink(where): os.unlink(where) os.symlink(os.path.join("..", "..", "archive", "example.org", "{0}{1}.pem".format(kind, ver)), where) with open(where, "w") as f: f.write(kind) self.assertEqual(self.test_rc.latest_common_version(), 5) self.assertEqual(self.test_rc.next_free_version(), 6) # Having one kind of file of a later version doesn't change the # result os.unlink(self.test_rc.privkey) os.symlink(os.path.join("..", "..", "archive", "example.org", "privkey7.pem"), self.test_rc.privkey) with open(self.test_rc.privkey, "w") as f: f.write("privkey") self.assertEqual(self.test_rc.latest_common_version(), 5) # ... although it does change the next free version self.assertEqual(self.test_rc.next_free_version(), 8) # Nor does having three out of four change the result os.unlink(self.test_rc.cert) os.symlink(os.path.join("..", "..", "archive", "example.org", "cert7.pem"), self.test_rc.cert) with open(self.test_rc.cert, "w") as f: f.write("cert") os.unlink(self.test_rc.fullchain) os.symlink(os.path.join("..", "..", "archive", "example.org", "fullchain7.pem"), self.test_rc.fullchain) with open(self.test_rc.fullchain, "w") as f: f.write("fullchain") self.assertEqual(self.test_rc.latest_common_version(), 5) # If we have everything from a much later version, it does change # the result ver = 17 for kind in ALL_FOUR: where = getattr(self.test_rc, kind) if os.path.islink(where): os.unlink(where) os.symlink(os.path.join("..", "..", "archive", "example.org", "{0}{1}.pem".format(kind, ver)), where) with open(where, "w") as f: f.write(kind) self.assertEqual(self.test_rc.latest_common_version(), 17) self.assertEqual(self.test_rc.next_free_version(), 18) def test_update_link_to(self): for ver in xrange(1, 6): for kind in ALL_FOUR: where = getattr(self.test_rc, kind) if os.path.islink(where): os.unlink(where) os.symlink(os.path.join("..", "..", "archive", "example.org", "{0}{1}.pem".format(kind, ver)), where) with open(where, "w") as f: f.write(kind) self.assertEqual(ver, self.test_rc.current_version(kind)) # pylint: disable=protected-access self.test_rc._update_link_to("cert", 3) self.test_rc._update_link_to("privkey", 2) self.assertEqual(3, self.test_rc.current_version("cert")) self.assertEqual(2, self.test_rc.current_version("privkey")) self.assertEqual(5, self.test_rc.current_version("chain")) self.assertEqual(5, self.test_rc.current_version("fullchain")) # Currently we are allowed to update to a version that doesn't exist self.test_rc._update_link_to("chain", 3000) # However, current_version doesn't allow querying the resulting # version (because it's a broken link). self.assertEqual(os.path.basename(os.readlink(self.test_rc.chain)), "chain3000.pem") def test_version(self): os.symlink(os.path.join("..", "..", "archive", "example.org", "cert12.pem"), self.test_rc.cert) with open(self.test_rc.cert, "w") as f: f.write("cert") # TODO: We should probably test that the directory is still the # same, but it's tricky because we can get an absolute # path out when we put a relative path in. self.assertEqual("cert8.pem", os.path.basename(self.test_rc.version("cert", 8))) def test_update_all_links_to_success(self): for ver in xrange(1, 6): for kind in ALL_FOUR: where = getattr(self.test_rc, kind) if os.path.islink(where): os.unlink(where) os.symlink(os.path.join("..", "..", "archive", "example.org", "{0}{1}.pem".format(kind, ver)), where) with open(where, "w") as f: f.write(kind) self.assertEqual(ver, self.test_rc.current_version(kind)) self.assertEqual(self.test_rc.latest_common_version(), 5) for ver in xrange(1, 6): self.test_rc.update_all_links_to(ver) for kind in ALL_FOUR: self.assertEqual(ver, self.test_rc.current_version(kind)) self.assertEqual(self.test_rc.latest_common_version(), 5) def test_update_all_links_to_partial_failure(self): def unlink_or_raise(path, real_unlink=os.unlink): # pylint: disable=missing-docstring basename = os.path.basename(path) if "fullchain" in basename and basename.startswith("prev"): raise ValueError else: real_unlink(path) self._write_out_ex_kinds() with mock.patch("letsencrypt.storage.os.unlink") as mock_unlink: mock_unlink.side_effect = unlink_or_raise self.assertRaises(ValueError, self.test_rc.update_all_links_to, 12) for kind in ALL_FOUR: self.assertEqual(self.test_rc.current_version(kind), 12) def test_update_all_links_to_full_failure(self): def unlink_or_raise(path, real_unlink=os.unlink): # pylint: disable=missing-docstring if "fullchain" in os.path.basename(path): raise ValueError else: real_unlink(path) self._write_out_ex_kinds() with mock.patch("letsencrypt.storage.os.unlink") as mock_unlink: mock_unlink.side_effect = unlink_or_raise self.assertRaises(ValueError, self.test_rc.update_all_links_to, 12) for kind in ALL_FOUR: self.assertEqual(self.test_rc.current_version(kind), 11) def test_has_pending_deployment(self): for ver in xrange(1, 6): for kind in ALL_FOUR: where = getattr(self.test_rc, kind) if os.path.islink(where): os.unlink(where) os.symlink(os.path.join("..", "..", "archive", "example.org", "{0}{1}.pem".format(kind, ver)), where) with open(where, "w") as f: f.write(kind) self.assertEqual(ver, self.test_rc.current_version(kind)) for ver in xrange(1, 6): self.test_rc.update_all_links_to(ver) for kind in ALL_FOUR: self.assertEqual(ver, self.test_rc.current_version(kind)) if ver < 5: self.assertTrue(self.test_rc.has_pending_deployment()) else: self.assertFalse(self.test_rc.has_pending_deployment()) def test_names(self): # Trying the current version test_cert = test_util.load_vector("cert-san.pem") os.symlink(os.path.join("..", "..", "archive", "example.org", "cert12.pem"), self.test_rc.cert) with open(self.test_rc.cert, "w") as f: f.write(test_cert) self.assertEqual(self.test_rc.names(), ["example.com", "www.example.com"]) # Trying a non-current version test_cert = test_util.load_vector("cert.pem") os.unlink(self.test_rc.cert) os.symlink(os.path.join("..", "..", "archive", "example.org", "cert15.pem"), self.test_rc.cert) with open(self.test_rc.cert, "w") as f: f.write(test_cert) self.assertEqual(self.test_rc.names(12), ["example.com", "www.example.com"]) # Trying missing cert os.unlink(self.test_rc.cert) self.assertRaises(errors.CertStorageError, self.test_rc.names) @mock.patch("letsencrypt.storage.datetime") def test_time_interval_judgments(self, mock_datetime): """Test should_autodeploy() and should_autorenew() on the basis of expiry time windows.""" test_cert = test_util.load_vector("cert.pem") self._write_out_ex_kinds() self.test_rc.update_all_links_to(12) with open(self.test_rc.cert, "w") as f: f.write(test_cert) self.test_rc.update_all_links_to(11) with open(self.test_rc.cert, "w") as f: f.write(test_cert) mock_datetime.timedelta = datetime.timedelta for (current_time, interval, result) in [ # 2014-12-13 12:00:00+00:00 (about 5 days prior to expiry) # Times that should result in autorenewal/autodeployment (1418472000, "2 months", True), (1418472000, "1 week", True), # Times that should not (1418472000, "4 days", False), (1418472000, "2 days", False), # 2009-05-01 12:00:00+00:00 (about 5 years prior to expiry) # Times that should result in autorenewal/autodeployment (1241179200, "7 years", True), (1241179200, "11 years 2 months", True), # Times that should not (1241179200, "8 hours", False), (1241179200, "2 days", False), (1241179200, "40 days", False), (1241179200, "9 months", False), # 2015-01-01 (after expiry has already happened, so all # intervals should cause autorenewal/autodeployment) (1420070400, "0 seconds", True), (1420070400, "10 seconds", True), (1420070400, "10 minutes", True), (1420070400, "10 weeks", True), (1420070400, "10 months", True), (1420070400, "10 years", True), (1420070400, "99 months", True), ]: sometime = datetime.datetime.utcfromtimestamp(current_time) mock_datetime.datetime.utcnow.return_value = sometime self.test_rc.configuration["deploy_before_expiry"] = interval self.test_rc.configuration["renew_before_expiry"] = interval self.assertEqual(self.test_rc.should_autodeploy(), result) self.assertEqual(self.test_rc.should_autorenew(), result) def test_autodeployment_is_enabled(self): self.assertTrue(self.test_rc.autodeployment_is_enabled()) self.test_rc.configuration["autodeploy"] = "1" self.assertTrue(self.test_rc.autodeployment_is_enabled()) self.test_rc.configuration["autodeploy"] = "0" self.assertFalse(self.test_rc.autodeployment_is_enabled()) def test_should_autodeploy(self): """Test should_autodeploy() on the basis of reasons other than expiry time window.""" # pylint: disable=too-many-statements # Autodeployment turned off self.test_rc.configuration["autodeploy"] = "0" self.assertFalse(self.test_rc.should_autodeploy()) self.test_rc.configuration["autodeploy"] = "1" # No pending deployment for ver in xrange(1, 6): for kind in ALL_FOUR: where = getattr(self.test_rc, kind) if os.path.islink(where): os.unlink(where) os.symlink(os.path.join("..", "..", "archive", "example.org", "{0}{1}.pem".format(kind, ver)), where) with open(where, "w") as f: f.write(kind) self.assertFalse(self.test_rc.should_autodeploy()) def test_autorenewal_is_enabled(self): self.assertTrue(self.test_rc.autorenewal_is_enabled()) self.test_rc.configuration["autorenew"] = "1" self.assertTrue(self.test_rc.autorenewal_is_enabled()) self.test_rc.configuration["autorenew"] = "0" self.assertFalse(self.test_rc.autorenewal_is_enabled()) @mock.patch("letsencrypt.storage.RenewableCert.ocsp_revoked") def test_should_autorenew(self, mock_ocsp): """Test should_autorenew on the basis of reasons other than expiry time window.""" # pylint: disable=too-many-statements # Autorenewal turned off self.test_rc.configuration["autorenew"] = "0" self.assertFalse(self.test_rc.should_autorenew()) self.test_rc.configuration["autorenew"] = "1" for kind in ALL_FOUR: where = getattr(self.test_rc, kind) os.symlink(os.path.join("..", "..", "archive", "example.org", "{0}12.pem".format(kind)), where) with open(where, "w") as f: f.write(kind) # Mandatory renewal on the basis of OCSP revocation mock_ocsp.return_value = True self.assertTrue(self.test_rc.should_autorenew()) mock_ocsp.return_value = False def test_save_successor(self): for ver in xrange(1, 6): for kind in ALL_FOUR: where = getattr(self.test_rc, kind) if os.path.islink(where): os.unlink(where) os.symlink(os.path.join("..", "..", "archive", "example.org", "{0}{1}.pem".format(kind, ver)), where) with open(where, "w") as f: f.write(kind) self.test_rc.update_all_links_to(3) self.assertEqual( 6, self.test_rc.save_successor(3, "new cert", None, "new chain", self.cli_config)) with open(self.test_rc.version("cert", 6)) as f: self.assertEqual(f.read(), "new cert") with open(self.test_rc.version("chain", 6)) as f: self.assertEqual(f.read(), "new chain") with open(self.test_rc.version("fullchain", 6)) as f: self.assertEqual(f.read(), "new cert" + "new chain") # version 6 of the key should be a link back to version 3 self.assertFalse(os.path.islink(self.test_rc.version("privkey", 3))) self.assertTrue(os.path.islink(self.test_rc.version("privkey", 6))) # Let's try two more updates self.assertEqual( 7, self.test_rc.save_successor(6, "again", None, "newer chain", self.cli_config)) self.assertEqual( 8, self.test_rc.save_successor(7, "hello", None, "other chain", self.cli_config)) # All of the subsequent versions should link directly to the original # privkey. for i in (6, 7, 8): self.assertTrue(os.path.islink(self.test_rc.version("privkey", i))) self.assertEqual("privkey3.pem", os.path.basename(os.readlink( self.test_rc.version("privkey", i)))) for kind in ALL_FOUR: self.assertEqual(self.test_rc.available_versions(kind), range(1, 9)) self.assertEqual(self.test_rc.current_version(kind), 3) # Test updating from latest version rather than old version self.test_rc.update_all_links_to(8) self.assertEqual( 9, self.test_rc.save_successor(8, "last", None, "attempt", self.cli_config)) for kind in ALL_FOUR: self.assertEqual(self.test_rc.available_versions(kind), range(1, 10)) self.assertEqual(self.test_rc.current_version(kind), 8) with open(self.test_rc.version("fullchain", 9)) as f: self.assertEqual(f.read(), "last" + "attempt") temp_config_file = os.path.join(self.cli_config.renewal_configs_dir, self.test_rc.lineagename) + ".conf.new" with open(temp_config_file, "w") as f: f.write("We previously crashed while writing me :(") # Test updating when providing a new privkey. The key should # be saved in a new file rather than creating a new symlink. self.assertEqual( 10, self.test_rc.save_successor(9, "with", "a", "key", self.cli_config)) self.assertTrue(os.path.exists(self.test_rc.version("privkey", 10))) self.assertFalse(os.path.islink(self.test_rc.version("privkey", 10))) self.assertFalse(os.path.exists(temp_config_file)) def test_new_lineage(self): """Test for new_lineage() class method.""" from letsencrypt import storage result = storage.RenewableCert.new_lineage( "the-lineage.com", "cert", "privkey", "chain", self.cli_config) # This consistency check tests most relevant properties about the # newly created cert lineage. # pylint: disable=protected-access self.assertTrue(result._consistent()) self.assertTrue(os.path.exists(os.path.join( self.cli_config.renewal_configs_dir, "the-lineage.com.conf"))) with open(result.fullchain) as f: self.assertEqual(f.read(), "cert" + "chain") # Let's do it again and make sure it makes a different lineage result = storage.RenewableCert.new_lineage( "the-lineage.com", "cert2", "privkey2", "chain2", self.cli_config) self.assertTrue(os.path.exists(os.path.join( self.cli_config.renewal_configs_dir, "the-lineage.com-0001.conf"))) # Now trigger the detection of already existing files os.mkdir(os.path.join( self.cli_config.live_dir, "the-lineage.com-0002")) self.assertRaises(errors.CertStorageError, storage.RenewableCert.new_lineage, "the-lineage.com", "cert3", "privkey3", "chain3", self.cli_config) os.mkdir(os.path.join(self.cli_config.archive_dir, "other-example.com")) self.assertRaises(errors.CertStorageError, storage.RenewableCert.new_lineage, "other-example.com", "cert4", "privkey4", "chain4", self.cli_config) # Make sure it can accept renewal parameters result = storage.RenewableCert.new_lineage( "the-lineage.com", "cert2", "privkey2", "chain2", self.cli_config) # TODO: Conceivably we could test that the renewal parameters actually # got saved def test_new_lineage_nonexistent_dirs(self): """Test that directories can be created if they don't exist.""" from letsencrypt import storage shutil.rmtree(self.cli_config.renewal_configs_dir) shutil.rmtree(self.cli_config.archive_dir) shutil.rmtree(self.cli_config.live_dir) storage.RenewableCert.new_lineage( "the-lineage.com", "cert2", "privkey2", "chain2", self.cli_config) self.assertTrue(os.path.exists( os.path.join( self.cli_config.renewal_configs_dir, "the-lineage.com.conf"))) self.assertTrue(os.path.exists(os.path.join( self.cli_config.live_dir, "the-lineage.com", "privkey.pem"))) self.assertTrue(os.path.exists(os.path.join( self.cli_config.archive_dir, "the-lineage.com", "privkey1.pem"))) @mock.patch("letsencrypt.storage.le_util.unique_lineage_name") def test_invalid_config_filename(self, mock_uln): from letsencrypt import storage mock_uln.return_value = "this_does_not_end_with_dot_conf", "yikes" self.assertRaises(errors.CertStorageError, storage.RenewableCert.new_lineage, "example.com", "cert", "privkey", "chain", self.cli_config) def test_bad_kind(self): self.assertRaises( errors.CertStorageError, self.test_rc.current_target, "elephant") self.assertRaises( errors.CertStorageError, self.test_rc.current_version, "elephant") self.assertRaises( errors.CertStorageError, self.test_rc.version, "elephant", 17) self.assertRaises( errors.CertStorageError, self.test_rc.available_versions, "elephant") self.assertRaises( errors.CertStorageError, self.test_rc.newest_available_version, "elephant") # pylint: disable=protected-access self.assertRaises( errors.CertStorageError, self.test_rc._update_link_to, "elephant", 17) def test_ocsp_revoked(self): # XXX: This is currently hardcoded to False due to a lack of an # OCSP server to test against. self.assertFalse(self.test_rc.ocsp_revoked()) def test_add_time_interval(self): from letsencrypt import storage # this month has 30 days, and the next year is a leap year time_1 = pytz.UTC.fromutc(datetime.datetime(2003, 11, 20, 11, 59, 21)) # this month has 31 days, and the next year is not a leap year time_2 = pytz.UTC.fromutc(datetime.datetime(2012, 10, 18, 21, 31, 16)) # in different time zone (GMT+8) time_3 = pytz.timezone('Asia/Shanghai').fromutc( datetime.datetime(2015, 10, 26, 22, 25, 41)) intended = { (time_1, ""): time_1, (time_2, ""): time_2, (time_3, ""): time_3, (time_1, "17 days"): time_1 + datetime.timedelta(17), (time_2, "17 days"): time_2 + datetime.timedelta(17), (time_1, "30"): time_1 + datetime.timedelta(30), (time_2, "30"): time_2 + datetime.timedelta(30), (time_1, "7 weeks"): time_1 + datetime.timedelta(49), (time_2, "7 weeks"): time_2 + datetime.timedelta(49), # 1 month is always 30 days, no matter which month it is (time_1, "1 month"): time_1 + datetime.timedelta(30), (time_2, "1 month"): time_2 + datetime.timedelta(31), # 1 year could be 365 or 366 days, depends on the year (time_1, "1 year"): time_1 + datetime.timedelta(366), (time_2, "1 year"): time_2 + datetime.timedelta(365), (time_1, "1 year 1 day"): time_1 + datetime.timedelta(367), (time_2, "1 year 1 day"): time_2 + datetime.timedelta(366), (time_1, "1 year-1 day"): time_1 + datetime.timedelta(365), (time_2, "1 year-1 day"): time_2 + datetime.timedelta(364), (time_1, "4 years"): time_1 + datetime.timedelta(1461), (time_2, "4 years"): time_2 + datetime.timedelta(1461), } for parameters, excepted in intended.items(): base_time, interval = parameters self.assertEqual(storage.add_time_interval(base_time, interval), excepted) def test_missing_cert(self): from letsencrypt import storage self.assertRaises(errors.CertStorageError, storage.RenewableCert, self.config.filename, self.cli_config) os.symlink("missing", self.config[ALL_FOUR[0]]) self.assertRaises(errors.CertStorageError, storage.RenewableCert, self.config.filename, self.cli_config) if __name__ == "__main__": unittest.main() # pragma: no cover letsencrypt-0.4.1/letsencrypt/tests/display/0000755000175000017500000000000012665157717020670 5ustar bmwbmw00000000000000letsencrypt-0.4.1/letsencrypt/tests/display/ops_test.py0000644000175000017500000004270212665157707023106 0ustar bmwbmw00000000000000# coding=utf-8 """Test letsencrypt.display.ops.""" import os import sys import tempfile import unittest import mock import zope.component from acme import jose from acme import messages from letsencrypt import account from letsencrypt import interfaces from letsencrypt.display import util as display_util from letsencrypt.tests import test_util KEY = jose.JWKRSA.load(test_util.load_vector("rsa512_key.pem")) class ChoosePluginTest(unittest.TestCase): """Tests for letsencrypt.display.ops.choose_plugin.""" def setUp(self): zope.component.provideUtility(display_util.FileDisplay(sys.stdout)) self.mock_apache = mock.Mock( description_with_name="a", misconfigured=True) self.mock_stand = mock.Mock( description_with_name="s", misconfigured=False) self.mock_stand.init().more_info.return_value = "standalone" self.plugins = [ self.mock_apache, self.mock_stand, ] def _call(self): from letsencrypt.display.ops import choose_plugin return choose_plugin(self.plugins, "Question?") @mock.patch("letsencrypt.display.ops.util") def test_selection(self, mock_util): mock_util().menu.side_effect = [(display_util.OK, 0), (display_util.OK, 1)] self.assertEqual(self.mock_stand, self._call()) self.assertEqual(mock_util().notification.call_count, 1) @mock.patch("letsencrypt.display.ops.util") def test_more_info(self, mock_util): mock_util().menu.side_effect = [ (display_util.HELP, 0), (display_util.HELP, 1), (display_util.OK, 1), ] self.assertEqual(self.mock_stand, self._call()) self.assertEqual(mock_util().notification.call_count, 2) @mock.patch("letsencrypt.display.ops.util") def test_no_choice(self, mock_util): mock_util().menu.return_value = (display_util.CANCEL, 0) self.assertTrue(self._call() is None) class PickPluginTest(unittest.TestCase): """Tests for letsencrypt.display.ops.pick_plugin.""" def setUp(self): self.config = mock.Mock(noninteractive_mode=False) self.default = None self.reg = mock.MagicMock() self.question = "Question?" self.ifaces = [] def _call(self): from letsencrypt.display.ops import pick_plugin return pick_plugin(self.config, self.default, self.reg, self.question, self.ifaces) def test_default_provided(self): self.default = "foo" self._call() self.assertEqual(1, self.reg.filter.call_count) def test_no_default(self): self._call() self.assertEqual(1, self.reg.visible().ifaces.call_count) def test_no_candidate(self): self.assertTrue(self._call() is None) def test_single(self): plugin_ep = mock.MagicMock() plugin_ep.init.return_value = "foo" plugin_ep.misconfigured = False self.reg.visible().ifaces().verify().available.return_value = { "bar": plugin_ep} self.assertEqual("foo", self._call()) def test_single_misconfigured(self): plugin_ep = mock.MagicMock() plugin_ep.init.return_value = "foo" plugin_ep.misconfigured = True self.reg.visible().ifaces().verify().available.return_value = { "bar": plugin_ep} self.assertTrue(self._call() is None) def test_multiple(self): plugin_ep = mock.MagicMock() plugin_ep.init.return_value = "foo" self.reg.visible().ifaces().verify().available.return_value = { "bar": plugin_ep, "baz": plugin_ep, } with mock.patch("letsencrypt.display.ops.choose_plugin") as mock_choose: mock_choose.return_value = plugin_ep self.assertEqual("foo", self._call()) mock_choose.assert_called_once_with( [plugin_ep, plugin_ep], self.question) def test_choose_plugin_none(self): self.reg.visible().ifaces().verify().available.return_value = { "bar": None, "baz": None, } with mock.patch("letsencrypt.display.ops.choose_plugin") as mock_choose: mock_choose.return_value = None self.assertTrue(self._call() is None) class ConveniencePickPluginTest(unittest.TestCase): """Tests for letsencrypt.display.ops.pick_*.""" def _test(self, fun, ifaces): config = mock.Mock() default = mock.Mock() plugins = mock.Mock() with mock.patch("letsencrypt.display.ops.pick_plugin") as mock_p: mock_p.return_value = "foo" self.assertEqual("foo", fun(config, default, plugins, "Question?")) mock_p.assert_called_once_with( config, default, plugins, "Question?", ifaces) def test_authenticator(self): from letsencrypt.display.ops import pick_authenticator self._test(pick_authenticator, (interfaces.IAuthenticator,)) def test_installer(self): from letsencrypt.display.ops import pick_installer self._test(pick_installer, (interfaces.IInstaller,)) def test_configurator(self): from letsencrypt.display.ops import pick_configurator self._test(pick_configurator, ( interfaces.IAuthenticator, interfaces.IInstaller)) class GetEmailTest(unittest.TestCase): """Tests for letsencrypt.display.ops.get_email.""" def setUp(self): mock_display = mock.MagicMock() self.input = mock_display.input zope.component.provideUtility(mock_display, interfaces.IDisplay) @classmethod def _call(cls, **kwargs): from letsencrypt.display.ops import get_email return get_email(**kwargs) def test_cancel_none(self): self.input.return_value = (display_util.CANCEL, "foo@bar.baz") self.assertTrue(self._call() is None) def test_ok_safe(self): self.input.return_value = (display_util.OK, "foo@bar.baz") with mock.patch("letsencrypt.display.ops.le_util.safe_email") as mock_safe_email: mock_safe_email.return_value = True self.assertTrue(self._call() is "foo@bar.baz") def test_ok_not_safe(self): self.input.return_value = (display_util.OK, "foo@bar.baz") with mock.patch("letsencrypt.display.ops.le_util.safe_email") as mock_safe_email: mock_safe_email.side_effect = [False, True] self.assertTrue(self._call() is "foo@bar.baz") def test_more_and_invalid_flags(self): more_txt = "--register-unsafely-without-email" invalid_txt = "There seem to be problems" base_txt = "Enter email" self.input.return_value = (display_util.OK, "foo@bar.baz") with mock.patch("letsencrypt.display.ops.le_util.safe_email") as mock_safe_email: mock_safe_email.return_value = True self._call() msg = self.input.call_args[0][0] self.assertTrue(more_txt not in msg) self.assertTrue(invalid_txt not in msg) self.assertTrue(base_txt in msg) self._call(more=True) msg = self.input.call_args[0][0] self.assertTrue(more_txt in msg) self.assertTrue(invalid_txt not in msg) self._call(more=True, invalid=True) msg = self.input.call_args[0][0] self.assertTrue(more_txt in msg) self.assertTrue(invalid_txt in msg) self.assertTrue(base_txt in msg) class ChooseAccountTest(unittest.TestCase): """Tests for letsencrypt.display.ops.choose_account.""" def setUp(self): zope.component.provideUtility(display_util.FileDisplay(sys.stdout)) self.accounts_dir = tempfile.mkdtemp("accounts") self.account_keys_dir = os.path.join(self.accounts_dir, "keys") os.makedirs(self.account_keys_dir, 0o700) self.config = mock.MagicMock( accounts_dir=self.accounts_dir, account_keys_dir=self.account_keys_dir, server="letsencrypt-demo.org") self.key = KEY self.acc1 = account.Account(messages.RegistrationResource( uri=None, new_authzr_uri=None, body=messages.Registration.from_data( email="email1@g.com")), self.key) self.acc2 = account.Account(messages.RegistrationResource( uri=None, new_authzr_uri=None, body=messages.Registration.from_data( email="email2@g.com", phone="phone")), self.key) @classmethod def _call(cls, accounts): from letsencrypt.display import ops return ops.choose_account(accounts) @mock.patch("letsencrypt.display.ops.util") def test_one(self, mock_util): mock_util().menu.return_value = (display_util.OK, 0) self.assertEqual(self._call([self.acc1]), self.acc1) @mock.patch("letsencrypt.display.ops.util") def test_two(self, mock_util): mock_util().menu.return_value = (display_util.OK, 1) self.assertEqual(self._call([self.acc1, self.acc2]), self.acc2) @mock.patch("letsencrypt.display.ops.util") def test_cancel(self, mock_util): mock_util().menu.return_value = (display_util.CANCEL, 1) self.assertTrue(self._call([self.acc1, self.acc2]) is None) class GenSSLLabURLs(unittest.TestCase): """Loose test of _gen_ssl_lab_urls. URL can change easily in the future.""" def setUp(self): zope.component.provideUtility(display_util.FileDisplay(sys.stdout)) @classmethod def _call(cls, domains): from letsencrypt.display.ops import _gen_ssl_lab_urls return _gen_ssl_lab_urls(domains) def test_zero(self): self.assertEqual(self._call([]), []) def test_two(self): urls = self._call(["eff.org", "umich.edu"]) self.assertTrue("eff.org" in urls[0]) self.assertTrue("umich.edu" in urls[1]) class GenHttpsNamesTest(unittest.TestCase): """Test _gen_https_names.""" def setUp(self): zope.component.provideUtility(display_util.FileDisplay(sys.stdout)) @classmethod def _call(cls, domains): from letsencrypt.display.ops import _gen_https_names return _gen_https_names(domains) def test_zero(self): self.assertEqual(self._call([]), "") def test_one(self): doms = [ "example.com", "asllkjsadfljasdf.c", ] for dom in doms: self.assertEqual(self._call([dom]), "https://%s" % dom) def test_two(self): domains_list = [ ["foo.bar.org", "bar.org"], ["paypal.google.facebook.live.com", "*.zombo.example.com"], ] for doms in domains_list: self.assertEqual( self._call(doms), "https://{dom[0]} and https://{dom[1]}".format(dom=doms)) def test_three(self): doms = ["a.org", "b.org", "c.org"] # We use an oxford comma self.assertEqual( self._call(doms), "https://{dom[0]}, https://{dom[1]}, and https://{dom[2]}".format( dom=doms)) def test_four(self): doms = ["a.org", "b.org", "c.org", "d.org"] exp = ("https://{dom[0]}, https://{dom[1]}, https://{dom[2]}, " "and https://{dom[3]}".format(dom=doms)) self.assertEqual(self._call(doms), exp) class ChooseNamesTest(unittest.TestCase): """Test choose names.""" def setUp(self): zope.component.provideUtility(display_util.FileDisplay(sys.stdout)) self.mock_install = mock.MagicMock() @classmethod def _call(cls, installer): from letsencrypt.display.ops import choose_names return choose_names(installer) @mock.patch("letsencrypt.display.ops._choose_names_manually") def test_no_installer(self, mock_manual): self._call(None) self.assertEqual(mock_manual.call_count, 1) @mock.patch("letsencrypt.display.ops.util") def test_no_installer_cancel(self, mock_util): mock_util().input.return_value = (display_util.CANCEL, []) self.assertEqual(self._call(None), []) @mock.patch("letsencrypt.display.ops.util") def test_no_names_choose(self, mock_util): self.mock_install().get_all_names.return_value = set() mock_util().yesno.return_value = True domain = "example.com" mock_util().input.return_value = (display_util.OK, domain) actual_doms = self._call(self.mock_install) self.assertEqual(mock_util().input.call_count, 1) self.assertEqual(actual_doms, [domain]) @mock.patch("letsencrypt.display.ops.util") def test_no_names_quit(self, mock_util): self.mock_install().get_all_names.return_value = set() mock_util().yesno.return_value = False self.assertEqual(self._call(self.mock_install), []) @mock.patch("letsencrypt.display.ops.util") def test_filter_names_valid_return(self, mock_util): self.mock_install.get_all_names.return_value = set(["example.com"]) mock_util().checklist.return_value = (display_util.OK, ["example.com"]) names = self._call(self.mock_install) self.assertEqual(names, ["example.com"]) self.assertEqual(mock_util().checklist.call_count, 1) @mock.patch("letsencrypt.display.ops.util") def test_filter_names_nothing_selected(self, mock_util): self.mock_install.get_all_names.return_value = set(["example.com"]) mock_util().checklist.return_value = (display_util.OK, []) self.assertEqual(self._call(self.mock_install), []) @mock.patch("letsencrypt.display.ops.util") def test_filter_names_cancel(self, mock_util): self.mock_install.get_all_names.return_value = set(["example.com"]) mock_util().checklist.return_value = ( display_util.CANCEL, ["example.com"]) self.assertEqual(self._call(self.mock_install), []) def test_get_valid_domains(self): from letsencrypt.display.ops import get_valid_domains all_valid = ["example.com", "second.example.com", "also.example.com"] all_invalid = ["xn--ls8h.tld", "*.wildcard.com", "notFQDN", "uniçodé.com"] two_valid = ["example.com", "xn--ls8h.tld", "also.example.com"] self.assertEqual(get_valid_domains(all_valid), all_valid) self.assertEqual(get_valid_domains(all_invalid), []) self.assertEqual(len(get_valid_domains(two_valid)), 2) @mock.patch("letsencrypt.display.ops.util") def test_choose_manually(self, mock_util): from letsencrypt.display.ops import _choose_names_manually # No retry mock_util().yesno.return_value = False # IDN and no retry mock_util().input.return_value = (display_util.OK, "uniçodé.com") self.assertEqual(_choose_names_manually(), []) # IDN exception with previous mocks with mock.patch( "letsencrypt.display.ops.display_util.separate_list_input" ) as mock_sli: unicode_error = UnicodeEncodeError('mock', u'', 0, 1, 'mock') mock_sli.side_effect = unicode_error self.assertEqual(_choose_names_manually(), []) # Punycode and no retry mock_util().input.return_value = (display_util.OK, "xn--ls8h.tld") self.assertEqual(_choose_names_manually(), []) # non-FQDN and no retry mock_util().input.return_value = (display_util.OK, "notFQDN") self.assertEqual(_choose_names_manually(), []) # Two valid domains mock_util().input.return_value = (display_util.OK, ("example.com," "valid.example.com")) self.assertEqual(_choose_names_manually(), ["example.com", "valid.example.com"]) # Three iterations mock_util().input.return_value = (display_util.OK, "notFQDN") yn = mock.MagicMock() yn.side_effect = [True, True, False] mock_util().yesno = yn _choose_names_manually() self.assertEqual(mock_util().yesno.call_count, 3) class SuccessInstallationTest(unittest.TestCase): # pylint: disable=too-few-public-methods """Test the success installation message.""" @classmethod def _call(cls, names): from letsencrypt.display.ops import success_installation success_installation(names) @mock.patch("letsencrypt.display.ops.util") def test_success_installation(self, mock_util): mock_util().notification.return_value = None names = ["example.com", "abc.com"] self._call(names) self.assertEqual(mock_util().notification.call_count, 1) arg = mock_util().notification.call_args_list[0][0][0] for name in names: self.assertTrue(name in arg) class SuccessRenewalTest(unittest.TestCase): # pylint: disable=too-few-public-methods """Test the success renewal message.""" @classmethod def _call(cls, names): from letsencrypt.display.ops import success_renewal success_renewal(names, "renew") @mock.patch("letsencrypt.display.ops.util") def test_success_renewal(self, mock_util): mock_util().notification.return_value = None names = ["example.com", "abc.com"] self._call(names) self.assertEqual(mock_util().notification.call_count, 1) arg = mock_util().notification.call_args_list[0][0][0] for name in names: self.assertTrue(name in arg) if __name__ == "__main__": unittest.main() # pragma: no cover letsencrypt-0.4.1/letsencrypt/tests/display/enhancements_test.py0000644000175000017500000000324712665157707024756 0ustar bmwbmw00000000000000"""Module for enhancement UI.""" import logging import unittest import mock from letsencrypt import errors from letsencrypt.display import util as display_util class AskTest(unittest.TestCase): """Test the ask method.""" def setUp(self): logging.disable(logging.CRITICAL) def tearDown(self): logging.disable(logging.NOTSET) @classmethod def _call(cls, enhancement): from letsencrypt.display.enhancements import ask return ask(enhancement) @mock.patch("letsencrypt.display.enhancements.util") def test_redirect(self, mock_util): mock_util().menu.return_value = (display_util.OK, 1) self.assertTrue(self._call("redirect")) def test_key_error(self): self.assertRaises(errors.Error, self._call, "unknown_enhancement") class RedirectTest(unittest.TestCase): """Test the redirect_by_default method.""" @classmethod def _call(cls): from letsencrypt.display.enhancements import redirect_by_default return redirect_by_default() @mock.patch("letsencrypt.display.enhancements.util") def test_secure(self, mock_util): mock_util().menu.return_value = (display_util.OK, 1) self.assertTrue(self._call()) @mock.patch("letsencrypt.display.enhancements.util") def test_cancel(self, mock_util): mock_util().menu.return_value = (display_util.CANCEL, 1) self.assertFalse(self._call()) @mock.patch("letsencrypt.display.enhancements.util") def test_easy(self, mock_util): mock_util().menu.return_value = (display_util.OK, 0) self.assertFalse(self._call()) if __name__ == "__main__": unittest.main() # pragma: no cover letsencrypt-0.4.1/letsencrypt/tests/display/util_test.py0000644000175000017500000003204212665157707023256 0ustar bmwbmw00000000000000"""Test :mod:`letsencrypt.display.util`.""" import os import unittest import mock import letsencrypt.errors as errors from letsencrypt.display import util as display_util CHOICES = [("First", "Description1"), ("Second", "Description2")] TAGS = ["tag1", "tag2", "tag3"] TAGS_CHOICES = [("1", "tag1"), ("2", "tag2"), ("3", "tag3")] class NcursesDisplayTest(unittest.TestCase): """Test ncurses display. Since this is mostly a wrapper, it might be more helpful to test the actual dialog boxes. The test file located in ./tests/display.py (relative to the root of the repository) will actually display the various boxes but requires the user to do the verification. If something seems amiss please use that test script to debug it, the automatic tests rely on too much mocking. """ def setUp(self): super(NcursesDisplayTest, self).setUp() self.displayer = display_util.NcursesDisplay() self.default_menu_options = { "choices": CHOICES, "ok_label": "OK", "cancel_label": "Cancel", "help_button": False, "help_label": "", "width": display_util.WIDTH, "height": display_util.HEIGHT, "menu_height": display_util.HEIGHT - 6, } @mock.patch("letsencrypt.display.util.dialog.Dialog.msgbox") def test_notification(self, mock_msgbox): """Kind of worthless... one liner.""" self.displayer.notification("message") self.assertEqual(mock_msgbox.call_count, 1) @mock.patch("letsencrypt.display.util.dialog.Dialog.menu") def test_menu_tag_and_desc(self, mock_menu): mock_menu.return_value = (display_util.OK, "First") ret = self.displayer.menu("Message", CHOICES) mock_menu.assert_called_with("Message", **self.default_menu_options) self.assertEqual(ret, (display_util.OK, 0)) @mock.patch("letsencrypt.display.util.dialog.Dialog.menu") def test_menu_tag_and_desc_cancel(self, mock_menu): mock_menu.return_value = (display_util.CANCEL, "") ret = self.displayer.menu("Message", CHOICES) mock_menu.assert_called_with("Message", **self.default_menu_options) self.assertEqual(ret, (display_util.CANCEL, -1)) @mock.patch("letsencrypt.display.util.dialog.Dialog.menu") def test_menu_desc_only(self, mock_menu): mock_menu.return_value = (display_util.OK, "1") ret = self.displayer.menu("Message", TAGS, help_label="More Info") self.default_menu_options.update( choices=TAGS_CHOICES, help_button=True, help_label="More Info") mock_menu.assert_called_with("Message", **self.default_menu_options) self.assertEqual(ret, (display_util.OK, 0)) @mock.patch("letsencrypt.display.util.dialog.Dialog.menu") def test_menu_desc_only_help(self, mock_menu): mock_menu.return_value = (display_util.HELP, "2") ret = self.displayer.menu("Message", TAGS, help_label="More Info") self.assertEqual(ret, (display_util.HELP, 1)) @mock.patch("letsencrypt.display.util.dialog.Dialog.menu") def test_menu_desc_only_cancel(self, mock_menu): mock_menu.return_value = (display_util.CANCEL, "") ret = self.displayer.menu("Message", TAGS, help_label="More Info") self.assertEqual(ret, (display_util.CANCEL, -1)) @mock.patch("letsencrypt.display.util." "dialog.Dialog.inputbox") def test_input(self, mock_input): self.displayer.input("message") self.assertEqual(mock_input.call_count, 1) @mock.patch("letsencrypt.display.util.dialog.Dialog.yesno") def test_yesno(self, mock_yesno): mock_yesno.return_value = display_util.OK self.assertTrue(self.displayer.yesno("message")) mock_yesno.assert_called_with( "message", display_util.HEIGHT, display_util.WIDTH, yes_label="Yes", no_label="No") @mock.patch("letsencrypt.display.util." "dialog.Dialog.checklist") def test_checklist(self, mock_checklist): self.displayer.checklist("message", TAGS) choices = [ (TAGS[0], "", True), (TAGS[1], "", True), (TAGS[2], "", True), ] mock_checklist.assert_called_with( "message", width=display_util.WIDTH, height=display_util.HEIGHT, choices=choices) class FileOutputDisplayTest(unittest.TestCase): """Test stdout display. Most of this class has to deal with visual output. In order to test how the functions look to a user, uncomment the test_visual function. """ def setUp(self): super(FileOutputDisplayTest, self).setUp() self.mock_stdout = mock.MagicMock() self.displayer = display_util.FileDisplay(self.mock_stdout) def test_notification_no_pause(self): self.displayer.notification("message", 10, False) string = self.mock_stdout.write.call_args[0][0] self.assertTrue("message" in string) def test_notification_pause(self): with mock.patch("__builtin__.raw_input", return_value="enter"): self.displayer.notification("message") self.assertTrue("message" in self.mock_stdout.write.call_args[0][0]) @mock.patch("letsencrypt.display.util." "FileDisplay._get_valid_int_ans") def test_menu(self, mock_ans): mock_ans.return_value = (display_util.OK, 1) ret = self.displayer.menu("message", CHOICES) self.assertEqual(ret, (display_util.OK, 0)) def test_input_cancel(self): with mock.patch("__builtin__.raw_input", return_value="c"): code, _ = self.displayer.input("message") self.assertTrue(code, display_util.CANCEL) def test_input_normal(self): with mock.patch("__builtin__.raw_input", return_value="domain.com"): code, input_ = self.displayer.input("message") self.assertEqual(code, display_util.OK) self.assertEqual(input_, "domain.com") def test_yesno(self): with mock.patch("__builtin__.raw_input", return_value="Yes"): self.assertTrue(self.displayer.yesno("message")) with mock.patch("__builtin__.raw_input", return_value="y"): self.assertTrue(self.displayer.yesno("message")) with mock.patch("__builtin__.raw_input", side_effect=["maybe", "y"]): self.assertTrue(self.displayer.yesno("message")) with mock.patch("__builtin__.raw_input", return_value="No"): self.assertFalse(self.displayer.yesno("message")) with mock.patch("__builtin__.raw_input", side_effect=["cancel", "n"]): self.assertFalse(self.displayer.yesno("message")) with mock.patch("__builtin__.raw_input", return_value="a"): self.assertTrue(self.displayer.yesno("msg", yes_label="Agree")) @mock.patch("letsencrypt.display.util.FileDisplay.input") def test_checklist_valid(self, mock_input): mock_input.return_value = (display_util.OK, "2 1") code, tag_list = self.displayer.checklist("msg", TAGS) self.assertEqual( (code, set(tag_list)), (display_util.OK, set(["tag1", "tag2"]))) @mock.patch("letsencrypt.display.util.FileDisplay.input") def test_checklist_miss_valid(self, mock_input): mock_input.side_effect = [ (display_util.OK, "10"), (display_util.OK, "tag1 please"), (display_util.OK, "1") ] ret = self.displayer.checklist("msg", TAGS) self.assertEqual(ret, (display_util.OK, ["tag1"])) @mock.patch("letsencrypt.display.util.FileDisplay.input") def test_checklist_miss_quit(self, mock_input): mock_input.side_effect = [ (display_util.OK, "10"), (display_util.CANCEL, "1") ] ret = self.displayer.checklist("msg", TAGS) self.assertEqual(ret, (display_util.CANCEL, [])) def test_scrub_checklist_input_valid(self): # pylint: disable=protected-access indices = [ ["1"], ["1", "2", "1"], ["2", "3"], ] exp = [ set(["tag1"]), set(["tag1", "tag2"]), set(["tag2", "tag3"]), ] for i, list_ in enumerate(indices): set_tags = set( self.displayer._scrub_checklist_input(list_, TAGS)) self.assertEqual(set_tags, exp[i]) def test_scrub_checklist_input_invalid(self): # pylint: disable=protected-access indices = [ ["0"], ["4"], ["tag1"], ["1", "tag1"], ["2", "o"] ] for list_ in indices: self.assertEqual( self.displayer._scrub_checklist_input(list_, TAGS), []) def test_print_menu(self): # pylint: disable=protected-access # This is purely cosmetic... just make sure there aren't any exceptions self.displayer._print_menu("msg", CHOICES) self.displayer._print_menu("msg", TAGS) def test_wrap_lines(self): # pylint: disable=protected-access msg = ("This is just a weak test{0}" "This function is only meant to be for easy viewing{0}" "Test a really really really really really really really really " "really really really really long line...".format(os.linesep)) text = display_util._wrap_lines(msg) self.assertEqual(text.count(os.linesep), 3) def test_get_valid_int_ans_valid(self): # pylint: disable=protected-access with mock.patch("__builtin__.raw_input", return_value="1"): self.assertEqual( self.displayer._get_valid_int_ans(1), (display_util.OK, 1)) ans = "2" with mock.patch("__builtin__.raw_input", return_value=ans): self.assertEqual( self.displayer._get_valid_int_ans(3), (display_util.OK, int(ans))) def test_get_valid_int_ans_invalid(self): # pylint: disable=protected-access answers = [ ["0", "c"], ["4", "one", "C"], ["c"], ] for ans in answers: with mock.patch("__builtin__.raw_input", side_effect=ans): self.assertEqual( self.displayer._get_valid_int_ans(3), (display_util.CANCEL, -1)) class NoninteractiveDisplayTest(unittest.TestCase): """Test non-interactive display. These tests are pretty easy! """ def setUp(self): super(NoninteractiveDisplayTest, self).setUp() self.mock_stdout = mock.MagicMock() self.displayer = display_util.NoninteractiveDisplay(self.mock_stdout) def test_notification_no_pause(self): self.displayer.notification("message", 10) string = self.mock_stdout.write.call_args[0][0] self.assertTrue("message" in string) def test_input(self): d = "an incomputable value" ret = self.displayer.input("message", default=d) self.assertEqual(ret, (display_util.OK, d)) self.assertRaises(errors.MissingCommandlineFlag, self.displayer.input, "message") def test_menu(self): ret = self.displayer.menu("message", CHOICES, default=1) self.assertEqual(ret, (display_util.OK, 1)) self.assertRaises(errors.MissingCommandlineFlag, self.displayer.menu, "message", CHOICES) def test_yesno(self): d = False ret = self.displayer.yesno("message", default=d) self.assertEqual(ret, d) self.assertRaises(errors.MissingCommandlineFlag, self.displayer.yesno, "message") def test_checklist(self): d = [1, 3] ret = self.displayer.checklist("message", TAGS, default=d) self.assertEqual(ret, (display_util.OK, d)) self.assertRaises(errors.MissingCommandlineFlag, self.displayer.checklist, "message", TAGS) class SeparateListInputTest(unittest.TestCase): """Test Module functions.""" def setUp(self): self.exp = ["a", "b", "c", "test"] @classmethod def _call(cls, input_): from letsencrypt.display.util import separate_list_input return separate_list_input(input_) def test_commas(self): self.assertEqual(self._call("a,b,c,test"), self.exp) def test_spaces(self): self.assertEqual(self._call("a b c test"), self.exp) def test_both(self): self.assertEqual(self._call("a, b, c, test"), self.exp) def test_mess(self): actual = [ self._call(" a , b c \t test"), self._call(",a, ,, , b c test "), self._call(",,,,, , a b,,, , c,test"), ] for act in actual: self.assertEqual(act, self.exp) class PlaceParensTest(unittest.TestCase): @classmethod def _call(cls, label): # pylint: disable=protected-access from letsencrypt.display.util import _parens_around_char return _parens_around_char(label) def test_single_letter(self): self.assertEqual("(a)", self._call("a")) def test_multiple(self): self.assertEqual("(L)abel", self._call("Label")) self.assertEqual("(y)es please", self._call("yes please")) if __name__ == "__main__": unittest.main() # pragma: no cover letsencrypt-0.4.1/letsencrypt/tests/display/__init__.py0000644000175000017500000000004212665157707022774 0ustar bmwbmw00000000000000"""Let's Encrypt Display Tests""" letsencrypt-0.4.1/letsencrypt/errors.py0000644000175000017500000000510512665157707017747 0ustar bmwbmw00000000000000"""Let's Encrypt client errors.""" class Error(Exception): """Generic Let's Encrypt client error.""" class AccountStorageError(Error): """Generic `.AccountStorage` error.""" class AccountNotFound(AccountStorageError): """Account not found error.""" class ReverterError(Error): """Let's Encrypt Reverter error.""" class SubprocessError(Error): """Subprocess handling error.""" class CertStorageError(Error): """Generic `.CertStorage` error.""" # Auth Handler Errors class AuthorizationError(Error): """Authorization error.""" class FailedChallenges(AuthorizationError): """Failed challenges error. :ivar set failed_achalls: Failed `.AnnotatedChallenge` instances. """ def __init__(self, failed_achalls): assert failed_achalls self.failed_achalls = failed_achalls super(FailedChallenges, self).__init__() def __str__(self): return "Failed authorization procedure. {0}".format( ", ".join( "{0} ({1}): {2}".format(achall.domain, achall.typ, achall.error) for achall in self.failed_achalls if achall.error is not None)) class ContAuthError(AuthorizationError): """Let's Encrypt Continuity Authenticator error.""" class DvAuthError(AuthorizationError): """Let's Encrypt DV Authenticator error.""" # Authenticator - Challenge specific errors class TLSSNI01Error(DvAuthError): """Let's Encrypt TLSSNI01 error.""" # Plugin Errors class PluginError(Error): """Let's Encrypt Plugin error.""" class PluginEnhancementAlreadyPresent(Error): """ Enhancement was already set """ class PluginSelectionError(Error): """A problem with plugin/configurator selection or setup""" class NoInstallationError(PluginError): """Let's Encrypt No Installation error.""" class MisconfigurationError(PluginError): """Let's Encrypt Misconfiguration error.""" class NotSupportedError(PluginError): """Let's Encrypt Plugin function not supported error.""" class RevokerError(Error): """Let's Encrypt Revoker error.""" class StandaloneBindError(Error): """Standalone plugin bind error.""" def __init__(self, socket_error, port): super(StandaloneBindError, self).__init__( "Problem binding to port {0}: {1}".format(port, socket_error)) self.socket_error = socket_error self.port = port class ConfigurationError(Error): """Configuration sanity error.""" # NoninteractiveDisplay iDisplay plugin error: class MissingCommandlineFlag(Error): """A command line argument was missing in noninteractive usage""" letsencrypt-0.4.1/letsencrypt/auth_handler.py0000644000175000017500000004662712665157707021107 0ustar bmwbmw00000000000000"""ACME AuthHandler.""" import itertools import logging import time import zope.component from acme import challenges from acme import messages from letsencrypt import achallenges from letsencrypt import constants from letsencrypt import errors from letsencrypt import error_handler from letsencrypt import interfaces logger = logging.getLogger(__name__) class AuthHandler(object): """ACME Authorization Handler for a client. :ivar dv_auth: Authenticator capable of solving :class:`~acme.challenges.DVChallenge` types :type dv_auth: :class:`letsencrypt.interfaces.IAuthenticator` :ivar cont_auth: Authenticator capable of solving :class:`~acme.challenges.ContinuityChallenge` types :type cont_auth: :class:`letsencrypt.interfaces.IAuthenticator` :ivar acme.client.Client acme: ACME client API. :ivar account: Client's Account :type account: :class:`letsencrypt.account.Account` :ivar dict authzr: ACME Authorization Resource dict where keys are domains and values are :class:`acme.messages.AuthorizationResource` :ivar list dv_c: DV challenges in the form of :class:`letsencrypt.achallenges.AnnotatedChallenge` :ivar list cont_c: Continuity challenges in the form of :class:`letsencrypt.achallenges.AnnotatedChallenge` """ def __init__(self, dv_auth, cont_auth, acme, account): self.dv_auth = dv_auth self.cont_auth = cont_auth self.acme = acme self.account = account self.authzr = dict() # List must be used to keep responses straight. self.dv_c = [] self.cont_c = [] def get_authorizations(self, domains, best_effort=False): """Retrieve all authorizations for challenges. :param list domains: Domains for authorization :param bool best_effort: Whether or not all authorizations are required (this is useful in renewal) :returns: tuple of lists of authorization resources. Takes the form of (`completed`, `failed`) :rtype: tuple :raises .AuthorizationError: If unable to retrieve all authorizations """ for domain in domains: self.authzr[domain] = self.acme.request_domain_challenges( domain, self.account.regr.new_authzr_uri) self._choose_challenges(domains) # While there are still challenges remaining... while self.dv_c or self.cont_c: cont_resp, dv_resp = self._solve_challenges() logger.info("Waiting for verification...") # Send all Responses - this modifies dv_c and cont_c self._respond(cont_resp, dv_resp, best_effort) # Just make sure all decisions are complete. self.verify_authzr_complete() # Only return valid authorizations return [authzr for authzr in self.authzr.values() if authzr.body.status == messages.STATUS_VALID] def _choose_challenges(self, domains): """Retrieve necessary challenges to satisfy server.""" logger.info("Performing the following challenges:") for dom in domains: path = gen_challenge_path( self.authzr[dom].body.challenges, self._get_chall_pref(dom), self.authzr[dom].body.combinations) dom_cont_c, dom_dv_c = self._challenge_factory( dom, path) self.dv_c.extend(dom_dv_c) self.cont_c.extend(dom_cont_c) def _solve_challenges(self): """Get Responses for challenges from authenticators.""" cont_resp = [] dv_resp = [] with error_handler.ErrorHandler(self._cleanup_challenges): try: if self.cont_c: cont_resp = self.cont_auth.perform(self.cont_c) if self.dv_c: dv_resp = self.dv_auth.perform(self.dv_c) except errors.AuthorizationError: logger.critical("Failure in setting up challenges.") logger.info("Attempting to clean up outstanding challenges...") raise assert len(cont_resp) == len(self.cont_c) assert len(dv_resp) == len(self.dv_c) return cont_resp, dv_resp def _respond(self, cont_resp, dv_resp, best_effort): """Send/Receive confirmation of all challenges. .. note:: This method also cleans up the auth_handler state. """ # TODO: chall_update is a dirty hack to get around acme-spec #105 chall_update = dict() active_achalls = [] active_achalls.extend( self._send_responses(self.dv_c, dv_resp, chall_update)) active_achalls.extend( self._send_responses(self.cont_c, cont_resp, chall_update)) # Check for updated status... try: self._poll_challenges(chall_update, best_effort) finally: # This removes challenges from self.dv_c and self.cont_c self._cleanup_challenges(active_achalls) def _send_responses(self, achalls, resps, chall_update): """Send responses and make sure errors are handled. :param dict chall_update: parameter that is updated to hold authzr -> list of outstanding solved annotated challenges """ active_achalls = [] for achall, resp in itertools.izip(achalls, resps): # This line needs to be outside of the if block below to # ensure failed challenges are cleaned up correctly active_achalls.append(achall) # Don't send challenges for None and False authenticator responses if resp is not None and resp: self.acme.answer_challenge(achall.challb, resp) # TODO: answer_challenge returns challr, with URI, # that can be used in _find_updated_challr # comparisons... if achall.domain in chall_update: chall_update[achall.domain].append(achall) else: chall_update[achall.domain] = [achall] return active_achalls def _poll_challenges( self, chall_update, best_effort, min_sleep=3, max_rounds=15): """Wait for all challenge results to be determined.""" dom_to_check = set(chall_update.keys()) comp_domains = set() rounds = 0 while dom_to_check and rounds < max_rounds: # TODO: Use retry-after... time.sleep(min_sleep) all_failed_achalls = set() for domain in dom_to_check: comp_achalls, failed_achalls = self._handle_check( domain, chall_update[domain]) if len(comp_achalls) == len(chall_update[domain]): comp_domains.add(domain) elif not failed_achalls: for achall, _ in comp_achalls: chall_update[domain].remove(achall) # We failed some challenges... damage control else: # Right now... just assume a loss and carry on... if best_effort: comp_domains.add(domain) else: all_failed_achalls.update( updated for _, updated in failed_achalls) if all_failed_achalls: _report_failed_challs(all_failed_achalls) raise errors.FailedChallenges(all_failed_achalls) dom_to_check -= comp_domains comp_domains.clear() rounds += 1 def _handle_check(self, domain, achalls): """Returns tuple of ('completed', 'failed').""" completed = [] failed = [] self.authzr[domain], _ = self.acme.poll(self.authzr[domain]) if self.authzr[domain].body.status == messages.STATUS_VALID: return achalls, [] # Note: if the whole authorization is invalid, the individual failed # challenges will be determined here... for achall in achalls: updated_achall = achall.update(challb=self._find_updated_challb( self.authzr[domain], achall)) # This does nothing for challenges that have yet to be decided yet. if updated_achall.status == messages.STATUS_VALID: completed.append((achall, updated_achall)) elif updated_achall.status == messages.STATUS_INVALID: failed.append((achall, updated_achall)) return completed, failed def _find_updated_challb(self, authzr, achall): # pylint: disable=no-self-use """Find updated challenge body within Authorization Resource. .. warning:: This assumes only one instance of type of challenge in each challenge resource. :param .AuthorizationResource authzr: Authorization Resource :param .AnnotatedChallenge achall: Annotated challenge for which to get status """ for authzr_challb in authzr.body.challenges: if type(authzr_challb.chall) is type(achall.challb.chall): # noqa return authzr_challb raise errors.AuthorizationError( "Target challenge not found in authorization resource") def _get_chall_pref(self, domain): """Return list of challenge preferences. :param str domain: domain for which you are requesting preferences """ # Make sure to make a copy... chall_prefs = [] chall_prefs.extend(self.cont_auth.get_chall_pref(domain)) chall_prefs.extend(self.dv_auth.get_chall_pref(domain)) return chall_prefs def _cleanup_challenges(self, achall_list=None): """Cleanup challenges. If achall_list is not provided, cleanup all achallenges. """ logger.info("Cleaning up challenges") if achall_list is None: dv_c = self.dv_c cont_c = self.cont_c else: dv_c = [achall for achall in achall_list if isinstance(achall.chall, challenges.DVChallenge)] cont_c = [achall for achall in achall_list if isinstance( achall.chall, challenges.ContinuityChallenge)] if dv_c: self.dv_auth.cleanup(dv_c) for achall in dv_c: self.dv_c.remove(achall) if cont_c: self.cont_auth.cleanup(cont_c) for achall in cont_c: self.cont_c.remove(achall) def verify_authzr_complete(self): """Verifies that all authorizations have been decided. :returns: Whether all authzr are complete :rtype: bool """ for authzr in self.authzr.values(): if (authzr.body.status != messages.STATUS_VALID and authzr.body.status != messages.STATUS_INVALID): raise errors.AuthorizationError("Incomplete authorizations") def _challenge_factory(self, domain, path): """Construct Namedtuple Challenges :param str domain: domain of the enrollee :param list path: List of indices from `challenges`. :returns: dv_chall, list of DVChallenge type :class:`letsencrypt.achallenges.Indexed` cont_chall, list of ContinuityChallenge type :class:`letsencrypt.achallenges.Indexed` :rtype: tuple :raises .errors.Error: if challenge type is not recognized """ dv_chall = [] cont_chall = [] for index in path: challb = self.authzr[domain].body.challenges[index] chall = challb.chall achall = challb_to_achall(challb, self.account.key, domain) if isinstance(chall, challenges.ContinuityChallenge): cont_chall.append(achall) elif isinstance(chall, challenges.DVChallenge): dv_chall.append(achall) return cont_chall, dv_chall def challb_to_achall(challb, account_key, domain): """Converts a ChallengeBody object to an AnnotatedChallenge. :param .ChallengeBody challb: ChallengeBody :param .JWK account_key: Authorized Account Key :param str domain: Domain of the challb :returns: Appropriate AnnotatedChallenge :rtype: :class:`letsencrypt.achallenges.AnnotatedChallenge` """ chall = challb.chall logger.info("%s challenge for %s", chall.typ, domain) if isinstance(chall, challenges.KeyAuthorizationChallenge): return achallenges.KeyAuthorizationAnnotatedChallenge( challb=challb, domain=domain, account_key=account_key) elif isinstance(chall, challenges.DNS): return achallenges.DNS(challb=challb, domain=domain) elif isinstance(chall, challenges.RecoveryContact): return achallenges.RecoveryContact( challb=challb, domain=domain) elif isinstance(chall, challenges.ProofOfPossession): return achallenges.ProofOfPossession( challb=challb, domain=domain) else: raise errors.Error( "Received unsupported challenge of type: %s", chall.typ) def gen_challenge_path(challbs, preferences, combinations): """Generate a plan to get authority over the identity. .. todo:: This can be possibly be rewritten to use resolved_combinations. :param tuple challbs: A tuple of challenges (:class:`acme.messages.Challenge`) from :class:`acme.messages.AuthorizationResource` to be fulfilled by the client in order to prove possession of the identifier. :param list preferences: List of challenge preferences for domain (:class:`acme.challenges.Challenge` subclasses) :param tuple combinations: A collection of sets of challenges from :class:`acme.messages.Challenge`, each of which would be sufficient to prove possession of the identifier. :returns: tuple of indices from ``challenges``. :rtype: tuple :raises letsencrypt.errors.AuthorizationError: If a path cannot be created that satisfies the CA given the preferences and combinations. """ if combinations: return _find_smart_path(challbs, preferences, combinations) else: return _find_dumb_path(challbs, preferences) def _find_smart_path(challbs, preferences, combinations): """Find challenge path with server hints. Can be called if combinations is included. Function uses a simple ranking system to choose the combo with the lowest cost. """ chall_cost = {} max_cost = 1 for i, chall_cls in enumerate(preferences): chall_cost[chall_cls] = i max_cost += i # max_cost is now equal to sum(indices) + 1 best_combo = [] # Set above completing all of the available challenges best_combo_cost = max_cost combo_total = 0 for combo in combinations: for challenge_index in combo: combo_total += chall_cost.get(challbs[ challenge_index].chall.__class__, max_cost) if combo_total < best_combo_cost: best_combo = combo best_combo_cost = combo_total combo_total = 0 if not best_combo: msg = ("Client does not support any combination of challenges that " "will satisfy the CA.") logger.fatal(msg) raise errors.AuthorizationError(msg) return best_combo def _find_dumb_path(challbs, preferences): """Find challenge path without server hints. Should be called if the combinations hint is not included by the server. This function returns the best path that does not contain multiple mutually exclusive challenges. """ assert len(preferences) == len(set(preferences)) path = [] satisfied = set() for pref_c in preferences: for i, offered_challb in enumerate(challbs): if (isinstance(offered_challb.chall, pref_c) and is_preferred(offered_challb, satisfied)): path.append(i) satisfied.add(offered_challb) return path def mutually_exclusive(obj1, obj2, groups, different=False): """Are two objects mutually exclusive?""" for group in groups: obj1_present = False obj2_present = False for obj_cls in group: obj1_present |= isinstance(obj1, obj_cls) obj2_present |= isinstance(obj2, obj_cls) if obj1_present and obj2_present and ( not different or not isinstance(obj1, obj2.__class__)): return False return True def is_preferred(offered_challb, satisfied, exclusive_groups=constants.EXCLUSIVE_CHALLENGES): """Return whether or not the challenge is preferred in path.""" for challb in satisfied: if not mutually_exclusive( offered_challb.chall, challb.chall, exclusive_groups, different=True): return False return True _ACME_PREFIX = "urn:acme:error:" _ERROR_HELP_COMMON = ( "To fix these errors, please make sure that your domain name was entered " "correctly and the DNS A record(s) for that domain contain(s) the " "right IP address.") _ERROR_HELP = { "connection": _ERROR_HELP_COMMON + " Additionally, please check that your computer " "has a publicly routable IP address and that no firewalls are preventing " "the server from communicating with the client. If you're using the " "webroot plugin, you should also verify that you are serving files " "from the webroot path you provided.", "dnssec": _ERROR_HELP_COMMON + " Additionally, if you have DNSSEC enabled for " "your domain, please ensure that the signature is valid.", "malformed": "To fix these errors, please make sure that you did not provide any " "invalid information to the client, and try running Let's Encrypt " "again.", "serverInternal": "Unfortunately, an error on the ACME server prevented you from completing " "authorization. Please try again later.", "tls": _ERROR_HELP_COMMON + " Additionally, please check that you have an " "up-to-date TLS configuration that allows the server to communicate " "with the Let's Encrypt client.", "unauthorized": _ERROR_HELP_COMMON, "unknownHost": _ERROR_HELP_COMMON, } def _report_failed_challs(failed_achalls): """Notifies the user about failed challenges. :param set failed_achalls: A set of failed :class:`letsencrypt.achallenges.AnnotatedChallenge`. """ problems = dict() for achall in failed_achalls: if achall.error: problems.setdefault(achall.error.typ, []).append(achall) reporter = zope.component.getUtility(interfaces.IReporter) for achalls in problems.itervalues(): reporter.add_message( _generate_failed_chall_msg(achalls), reporter.MEDIUM_PRIORITY) def _generate_failed_chall_msg(failed_achalls): """Creates a user friendly error message about failed challenges. :param list failed_achalls: A list of failed :class:`letsencrypt.achallenges.AnnotatedChallenge` with the same error type. :returns: A formatted error message for the client. :rtype: str """ typ = failed_achalls[0].error.typ if typ.startswith(_ACME_PREFIX): typ = typ[len(_ACME_PREFIX):] msg = ["The following errors were reported by the server:"] for achall in failed_achalls: msg.append("\n\nDomain: %s\nType: %s\nDetail: %s" % ( achall.domain, typ, achall.error.detail)) if typ in _ERROR_HELP: msg.append("\n\n") msg.append(_ERROR_HELP[typ]) return "".join(msg) letsencrypt-0.4.1/letsencrypt/storage.py0000644000175000017500000010353412665157707020104 0ustar bmwbmw00000000000000"""Renewable certificates storage.""" import datetime import logging import os import re import configobj import parsedatetime import pytz from letsencrypt import constants from letsencrypt import crypto_util from letsencrypt import errors from letsencrypt import error_handler from letsencrypt import le_util logger = logging.getLogger(__name__) ALL_FOUR = ("cert", "privkey", "chain", "fullchain") def config_with_defaults(config=None): """Merge supplied config, if provided, on top of builtin defaults.""" defaults_copy = configobj.ConfigObj(constants.RENEWER_DEFAULTS) defaults_copy.merge(config if config is not None else configobj.ConfigObj()) return defaults_copy def add_time_interval(base_time, interval, textparser=parsedatetime.Calendar()): """Parse the time specified time interval, and add it to the base_time The interval can be in the English-language format understood by parsedatetime, e.g., '10 days', '3 weeks', '6 months', '9 hours', or a sequence of such intervals like '6 months 1 week' or '3 days 12 hours'. If an integer is found with no associated unit, it is interpreted by default as a number of days. :param datetime.datetime base_time: The time to be added with the interval. :param str interval: The time interval to parse. :returns: The base_time plus the interpretation of the time interval. :rtype: :class:`datetime.datetime`""" if interval.strip().isdigit(): interval += " days" # try to use the same timezone, but fallback to UTC tzinfo = base_time.tzinfo or pytz.UTC return textparser.parseDT(interval, base_time, tzinfo=tzinfo)[0] def write_renewal_config(filename, target, cli_config): """Writes a renewal config file with the specified name and values. :param str filename: Absolute path to the config file :param dict target: Maps ALL_FOUR to their symlink paths :param .RenewerConfiguration cli_config: parsed command line arguments :returns: Configuration object for the new config file :rtype: configobj.ConfigObj """ # create_empty creates a new config file if filename does not exist config = configobj.ConfigObj(filename, create_empty=True) for kind in ALL_FOUR: config[kind] = target[kind] # XXX: We clearly need a more general and correct way of getting # options into the configobj for the RenewableCert instance. # This is a quick-and-dirty way to do it to allow integration # testing to start. (Note that the config parameter to new_lineage # ideally should be a ConfigObj, but in this case a dict will be # accepted in practice.) renewalparams = vars(cli_config.namespace) if renewalparams: config["renewalparams"] = renewalparams config.comments["renewalparams"] = ["", "Options and defaults used" " in the renewal process"] # TODO: add human-readable comments explaining other available # parameters logger.debug("Writing new config %s.", filename) config.write() return config def update_configuration(lineagename, target, cli_config): """Modifies lineagename's config to contain the specified values. :param str lineagename: Name of the lineage being modified :param dict target: Maps ALL_FOUR to their symlink paths :param .RenewerConfiguration cli_config: parsed command line arguments :returns: Configuration object for the updated config file :rtype: configobj.ConfigObj """ config_filename = os.path.join( cli_config.renewal_configs_dir, lineagename) + ".conf" temp_filename = config_filename + ".new" # If an existing tempfile exists, delete it if os.path.exists(temp_filename): os.unlink(temp_filename) write_renewal_config(temp_filename, target, cli_config) os.rename(temp_filename, config_filename) return configobj.ConfigObj(config_filename) def get_link_target(link): """Get an absolute path to the target of link. :param str link: Path to a symbolic link :returns: Absolute path to the target of link :rtype: str """ target = os.readlink(link) if not os.path.isabs(target): target = os.path.join(os.path.dirname(link), target) return os.path.abspath(target) class RenewableCert(object): # pylint: disable=too-many-instance-attributes """Renewable certificate. Represents a lineage of certificates that is under the management of the Let's Encrypt client, indicated by the existence of an associated renewal configuration file. Note that the notion of "current version" for a lineage is maintained on disk in the structure of symbolic links, and is not explicitly stored in any instance variable in this object. The RenewableCert object is able to determine information about the current (or other) version by accessing data on disk, but does not inherently know any of this information except by examining the symbolic links as needed. The instance variables mentioned below point to symlinks that reflect the notion of "current version" of each managed object, and it is these paths that should be used when configuring servers to use the certificate managed in a lineage. These paths are normally within the "live" directory, and their symlink targets -- the actual cert files -- are normally found within the "archive" directory. :ivar str cert: The path to the symlink representing the current version of the certificate managed by this lineage. :ivar str privkey: The path to the symlink representing the current version of the private key managed by this lineage. :ivar str chain: The path to the symlink representing the current version of the chain managed by this lineage. :ivar str fullchain: The path to the symlink representing the current version of the fullchain (combined chain and cert) managed by this lineage. :ivar configobj.ConfigObj configuration: The renewal configuration options associated with this lineage, obtained from parsing the renewal configuration file and/or systemwide defaults. """ def __init__(self, config_filename, cli_config): """Instantiate a RenewableCert object from an existing lineage. :param str config_filename: the path to the renewal config file that defines this lineage. :param .RenewerConfiguration: parsed command line arguments :raises .CertStorageError: if the configuration file's name didn't end in ".conf", or the file is missing or broken. """ self.cli_config = cli_config if not config_filename.endswith(".conf"): raise errors.CertStorageError( "renewal config file name must end in .conf") self.lineagename = os.path.basename( config_filename[:-len(".conf")]) # self.configuration should be used to read parameters that # may have been chosen based on default values from the # systemwide renewal configuration; self.configfile should be # used to make and save changes. try: self.configfile = configobj.ConfigObj(config_filename) except configobj.ConfigObjError: raise errors.CertStorageError( "error parsing {0}".format(config_filename)) # TODO: Do we actually use anything from defaults and do we want to # read further defaults from the systemwide renewal configuration # file at this stage? self.configuration = config_with_defaults(self.configfile) if not all(x in self.configuration for x in ALL_FOUR): raise errors.CertStorageError( "renewal config file {0} is missing a required " "file reference".format(self.configfile)) self.cert = self.configuration["cert"] self.privkey = self.configuration["privkey"] self.chain = self.configuration["chain"] self.fullchain = self.configuration["fullchain"] self._fix_symlinks() self._check_symlinks() def _check_symlinks(self): """Raises an exception if a symlink doesn't exist""" for kind in ALL_FOUR: link = getattr(self, kind) if not os.path.islink(link): raise errors.CertStorageError( "expected {0} to be a symlink".format(link)) target = get_link_target(link) if not os.path.exists(target): raise errors.CertStorageError("target {0} of symlink {1} does " "not exist".format(target, link)) def _consistent(self): """Are the files associated with this lineage self-consistent? :returns: Whether the files stored in connection with this lineage appear to be correct and consistent with one another. :rtype: bool """ # Each element must be referenced with an absolute path for x in (self.cert, self.privkey, self.chain, self.fullchain): if not os.path.isabs(x): logger.debug("Element %s is not referenced with an " "absolute path.", x) return False # Each element must exist and be a symbolic link for x in (self.cert, self.privkey, self.chain, self.fullchain): if not os.path.islink(x): logger.debug("Element %s is not a symbolic link.", x) return False for kind in ALL_FOUR: link = getattr(self, kind) target = get_link_target(link) # Each element's link must point within the cert lineage's # directory within the official archive directory desired_directory = os.path.join( self.cli_config.archive_dir, self.lineagename) if not os.path.samefile(os.path.dirname(target), desired_directory): logger.debug("Element's link does not point within the " "cert lineage's directory within the " "official archive directory. Link: %s, " "target directory: %s, " "archive directory: %s.", link, os.path.dirname(target), desired_directory) return False # The link must point to a file that exists if not os.path.exists(target): logger.debug("Link %s points to file %s that does not exist.", link, target) return False # The link must point to a file that follows the archive # naming convention pattern = re.compile(r"^{0}([0-9]+)\.pem$".format(kind)) if not pattern.match(os.path.basename(target)): logger.debug("%s does not follow the archive naming " "convention.", target) return False # It is NOT required that the link's target be a regular # file (it may itself be a symlink). But we should probably # do a recursive check that ultimately the target does # exist? # XXX: Additional possible consistency checks (e.g. # cryptographic validation of the chain being a chain, # the chain matching the cert, and the cert matching # the subject key) # XXX: All four of the targets are in the same directory # (This check is redundant with the check that they # are all in the desired directory!) # len(set(os.path.basename(self.current_target(x) # for x in ALL_FOUR))) == 1 return True def _fix(self): """Attempt to fix defects or inconsistencies in this lineage. .. todo:: Currently unimplemented. """ # TODO: Figure out what kinds of fixes are possible. For # example, checking if there is a valid version that # we can update the symlinks to. (Maybe involve # parsing keys and certs to see if they exist and # if a key corresponds to the subject key of a cert?) # TODO: In general, the symlink-reading functions below are not # cautious enough about the possibility that links or their # targets may not exist. (This shouldn't happen, but might # happen as a result of random tampering by a sysadmin, or # filesystem errors, or crashes.) def _previous_symlinks(self): """Returns the kind and path of all symlinks used in recovery. :returns: list of (kind, symlink) tuples :rtype: list """ previous_symlinks = [] for kind in ALL_FOUR: link_dir = os.path.dirname(getattr(self, kind)) link_base = "previous_{0}.pem".format(kind) previous_symlinks.append((kind, os.path.join(link_dir, link_base))) return previous_symlinks def _fix_symlinks(self): """Fixes symlinks in the event of an incomplete version update. If there is no problem with the current symlinks, this function has no effect. """ previous_symlinks = self._previous_symlinks() if all(os.path.exists(link[1]) for link in previous_symlinks): for kind, previous_link in previous_symlinks: current_link = getattr(self, kind) if os.path.lexists(current_link): os.unlink(current_link) os.symlink(os.readlink(previous_link), current_link) for _, link in previous_symlinks: if os.path.exists(link): os.unlink(link) def current_target(self, kind): """Returns full path to which the specified item currently points. :param str kind: the lineage member item ("cert", "privkey", "chain", or "fullchain") :returns: The path to the current version of the specified member. :rtype: str or None """ if kind not in ALL_FOUR: raise errors.CertStorageError("unknown kind of item") link = getattr(self, kind) if not os.path.exists(link): logger.debug("Expected symlink %s for %s does not exist.", link, kind) return None return get_link_target(link) def current_version(self, kind): """Returns numerical version of the specified item. For example, if kind is "chain" and the current chain link points to a file named "chain7.pem", returns the integer 7. :param str kind: the lineage member item ("cert", "privkey", "chain", or "fullchain") :returns: the current version of the specified member. :rtype: int """ if kind not in ALL_FOUR: raise errors.CertStorageError("unknown kind of item") pattern = re.compile(r"^{0}([0-9]+)\.pem$".format(kind)) target = self.current_target(kind) if target is None or not os.path.exists(target): logger.debug("Current-version target for %s " "does not exist at %s.", kind, target) target = "" matches = pattern.match(os.path.basename(target)) if matches: return int(matches.groups()[0]) else: logger.debug("No matches for target %s.", kind) return None def version(self, kind, version): """The filename that corresponds to the specified version and kind. .. warning:: The specified version may not exist in this lineage. There is no guarantee that the file path returned by this method actually exists. :param str kind: the lineage member item ("cert", "privkey", "chain", or "fullchain") :param int version: the desired version :returns: The path to the specified version of the specified member. :rtype: str """ if kind not in ALL_FOUR: raise errors.CertStorageError("unknown kind of item") where = os.path.dirname(self.current_target(kind)) return os.path.join(where, "{0}{1}.pem".format(kind, version)) def available_versions(self, kind): """Which alternative versions of the specified kind of item exist? The archive directory where the current version is stored is consulted to obtain the list of alternatives. :param str kind: the lineage member item ( ``cert``, ``privkey``, ``chain``, or ``fullchain``) :returns: all of the version numbers that currently exist :rtype: `list` of `int` """ if kind not in ALL_FOUR: raise errors.CertStorageError("unknown kind of item") where = os.path.dirname(self.current_target(kind)) files = os.listdir(where) pattern = re.compile(r"^{0}([0-9]+)\.pem$".format(kind)) matches = [pattern.match(f) for f in files] return sorted([int(m.groups()[0]) for m in matches if m]) def newest_available_version(self, kind): """Newest available version of the specified kind of item? :param str kind: the lineage member item (``cert``, ``privkey``, ``chain``, or ``fullchain``) :returns: the newest available version of this member :rtype: int """ return max(self.available_versions(kind)) def latest_common_version(self): """Newest version for which all items are available? :returns: the newest available version for which all members (``cert, ``privkey``, ``chain``, and ``fullchain``) exist :rtype: int """ # TODO: this can raise CertStorageError if there is no version overlap # (it should probably return None instead) # TODO: this can raise a spurious AttributeError if the current # link for any kind is missing (it should probably return None) versions = [self.available_versions(x) for x in ALL_FOUR] return max(n for n in versions[0] if all(n in v for v in versions[1:])) def next_free_version(self): """Smallest version newer than all full or partial versions? :returns: the smallest version number that is larger than any version of any item currently stored in this lineage :rtype: int """ # TODO: consider locking/mutual exclusion between updating processes # This isn't self.latest_common_version() + 1 because we don't want # collide with a version that might exist for one file type but not # for the others. return max(self.newest_available_version(x) for x in ALL_FOUR) + 1 def has_pending_deployment(self): """Is there a later version of all of the managed items? :returns: ``True`` if there is a complete version of this lineage with a larger version number than the current version, and ``False`` otherwis :rtype: bool """ # TODO: consider whether to assume consistency or treat # inconsistent/consistent versions differently smallest_current = min(self.current_version(x) for x in ALL_FOUR) return smallest_current < self.latest_common_version() def _update_link_to(self, kind, version): """Make the specified item point at the specified version. (Note that this method doesn't verify that the specified version exists.) :param str kind: the lineage member item ("cert", "privkey", "chain", or "fullchain") :param int version: the desired version """ if kind not in ALL_FOUR: raise errors.CertStorageError("unknown kind of item") link = getattr(self, kind) filename = "{0}{1}.pem".format(kind, version) # Relative rather than absolute target directory target_directory = os.path.dirname(os.readlink(link)) # TODO: it could be safer to make the link first under a temporary # filename, then unlink the old link, then rename the new link # to the old link; this ensures that this process is able to # create symlinks. # TODO: we might also want to check consistency of related links # for the other corresponding items os.unlink(link) os.symlink(os.path.join(target_directory, filename), link) def update_all_links_to(self, version): """Change all member objects to point to the specified version. :param int version: the desired version """ with error_handler.ErrorHandler(self._fix_symlinks): previous_links = self._previous_symlinks() for kind, link in previous_links: os.symlink(self.current_target(kind), link) for kind in ALL_FOUR: self._update_link_to(kind, version) for _, link in previous_links: os.unlink(link) def names(self, version=None): """What are the subject names of this certificate? (If no version is specified, use the current version.) :param int version: the desired version number :returns: the subject names :rtype: `list` of `str` :raises .CertStorageError: if could not find cert file. """ if version is None: target = self.current_target("cert") else: target = self.version("cert", version) if target is None: raise errors.CertStorageError("could not find cert file") with open(target) as f: return crypto_util.get_sans_from_cert(f.read()) def autodeployment_is_enabled(self): """Is automatic deployment enabled for this cert? If autodeploy is not specified, defaults to True. :returns: True if automatic deployment is enabled :rtype: bool """ return ("autodeploy" not in self.configuration or self.configuration.as_bool("autodeploy")) def should_autodeploy(self, interactive=False): """Should this lineage now automatically deploy a newer version? This is a policy question and does not only depend on whether there is a newer version of the cert. (This considers whether autodeployment is enabled, whether a relevant newer version exists, and whether the time interval for autodeployment has been reached.) :param bool interactive: set to True to examine the question regardless of whether the renewal configuration allows automated deployment (for interactive use). Default False. :returns: whether the lineage now ought to autodeploy an existing newer cert version :rtype: bool """ if interactive or self.autodeployment_is_enabled(): if self.has_pending_deployment(): interval = self.configuration.get("deploy_before_expiry", "5 days") expiry = crypto_util.notAfter(self.current_target("cert")) now = pytz.UTC.fromutc(datetime.datetime.utcnow()) if expiry < add_time_interval(now, interval): return True return False def ocsp_revoked(self, version=None): # pylint: disable=no-self-use,unused-argument """Is the specified cert version revoked according to OCSP? Also returns True if the cert version is declared as intended to be revoked according to Let's Encrypt OCSP extensions. (If no version is specified, uses the current version.) This method is not yet implemented and currently always returns False. :param int version: the desired version number :returns: whether the certificate is or will be revoked :rtype: bool """ # XXX: This query and its associated network service aren't # implemented yet, so we currently return False (indicating that the # certificate is not revoked). return False def autorenewal_is_enabled(self): """Is automatic renewal enabled for this cert? If autorenew is not specified, defaults to True. :returns: True if automatic renewal is enabled :rtype: bool """ return ("autorenew" not in self.configuration or self.configuration.as_bool("autorenew")) def should_autorenew(self, interactive=False): """Should we now try to autorenew the most recent cert version? This is a policy question and does not only depend on whether the cert is expired. (This considers whether autorenewal is enabled, whether the cert is revoked, and whether the time interval for autorenewal has been reached.) Note that this examines the numerically most recent cert version, not the currently deployed version. :param bool interactive: set to True to examine the question regardless of whether the renewal configuration allows automated renewal (for interactive use). Default False. :returns: whether an attempt should now be made to autorenew the most current cert version in this lineage :rtype: bool """ if interactive or self.autorenewal_is_enabled(): # Consider whether to attempt to autorenew this cert now # Renewals on the basis of revocation if self.ocsp_revoked(self.latest_common_version()): logger.debug("Should renew, certificate is revoked.") return True # Renews some period before expiry time default_interval = constants.RENEWER_DEFAULTS["renew_before_expiry"] interval = self.configuration.get("renew_before_expiry", default_interval) expiry = crypto_util.notAfter(self.version( "cert", self.latest_common_version())) now = pytz.UTC.fromutc(datetime.datetime.utcnow()) if expiry < add_time_interval(now, interval): logger.debug("Should renew, less than %s before certificate " "expiry %s.", interval, expiry.strftime("%Y-%m-%d %H:%M:%S %Z")) return True return False @classmethod def new_lineage(cls, lineagename, cert, privkey, chain, cli_config): # pylint: disable=too-many-locals """Create a new certificate lineage. Attempts to create a certificate lineage -- enrolled for potential future renewal -- with the (suggested) lineage name lineagename, and the associated cert, privkey, and chain (the associated fullchain will be created automatically). Optional configurator and renewalparams record the configuration that was originally used to obtain this cert, so that it can be reused later during automated renewal. Returns a new RenewableCert object referring to the created lineage. (The actual lineage name, as well as all the relevant file paths, will be available within this object.) :param str lineagename: the suggested name for this lineage (normally the current cert's first subject DNS name) :param str cert: the initial certificate version in PEM format :param str privkey: the private key in PEM format :param str chain: the certificate chain in PEM format :param .RenewerConfiguration cli_config: parsed command line arguments :returns: the newly-created RenewalCert object :rtype: :class:`storage.renewableCert` """ # Examine the configuration and find the new lineage's name for i in (cli_config.renewal_configs_dir, cli_config.archive_dir, cli_config.live_dir): if not os.path.exists(i): os.makedirs(i, 0700) logger.debug("Creating directory %s.", i) config_file, config_filename = le_util.unique_lineage_name( cli_config.renewal_configs_dir, lineagename) if not config_filename.endswith(".conf"): raise errors.CertStorageError( "renewal config file name must end in .conf") # Determine where on disk everything will go # lineagename will now potentially be modified based on which # renewal configuration file could actually be created lineagename = os.path.basename(config_filename)[:-len(".conf")] archive = os.path.join(cli_config.archive_dir, lineagename) live_dir = os.path.join(cli_config.live_dir, lineagename) if os.path.exists(archive): raise errors.CertStorageError( "archive directory exists for " + lineagename) if os.path.exists(live_dir): raise errors.CertStorageError( "live directory exists for " + lineagename) os.mkdir(archive) os.mkdir(live_dir) logger.debug("Archive directory %s and live " "directory %s created.", archive, live_dir) relative_archive = os.path.join("..", "..", "archive", lineagename) # Put the data into the appropriate files on disk target = dict([(kind, os.path.join(live_dir, kind + ".pem")) for kind in ALL_FOUR]) for kind in ALL_FOUR: os.symlink(os.path.join(relative_archive, kind + "1.pem"), target[kind]) with open(target["cert"], "w") as f: logger.debug("Writing certificate to %s.", target["cert"]) f.write(cert) with open(target["privkey"], "w") as f: logger.debug("Writing private key to %s.", target["privkey"]) f.write(privkey) # XXX: Let's make sure to get the file permissions right here with open(target["chain"], "w") as f: logger.debug("Writing chain to %s.", target["chain"]) f.write(chain) with open(target["fullchain"], "w") as f: # assumes that OpenSSL.crypto.dump_certificate includes # ending newline character logger.debug("Writing full chain to %s.", target["fullchain"]) f.write(cert + chain) # Document what we've done in a new renewal config file config_file.close() new_config = write_renewal_config(config_filename, target, cli_config) return cls(new_config.filename, cli_config) def save_successor(self, prior_version, new_cert, new_privkey, new_chain, cli_config): """Save new cert and chain as a successor of a prior version. Returns the new version number that was created. .. note:: this function does NOT update links to deploy this version :param int prior_version: the old version to which this version is regarded as a successor (used to choose a privkey, if the key has not changed, but otherwise this information is not permanently recorded anywhere) :param str new_cert: the new certificate, in PEM format :param str new_privkey: the new private key, in PEM format, or ``None``, if the private key has not changed :param str new_chain: the new chain, in PEM format :param .RenewerConfiguration cli_config: parsed command line arguments :returns: the new version number that was created :rtype: int """ # XXX: assumes official archive location rather than examining links # XXX: consider using os.open for availability of os.O_EXCL # XXX: ensure file permissions are correct; also create directories # if needed (ensuring their permissions are correct) # Figure out what the new version is and hence where to save things self.cli_config = cli_config target_version = self.next_free_version() archive = self.cli_config.archive_dir # XXX if anyone ever moves a renewal configuration file, this will # break... perhaps prefix should be the dirname of the previous # cert.pem? prefix = os.path.join(archive, self.lineagename) target = dict( [(kind, os.path.join(prefix, "{0}{1}.pem".format(kind, target_version))) for kind in ALL_FOUR]) # Distinguish the cases where the privkey has changed and where it # has not changed (in the latter case, making an appropriate symlink # to an earlier privkey version) if new_privkey is None: # The behavior below keeps the prior key by creating a new # symlink to the old key or the target of the old key symlink. old_privkey = os.path.join( prefix, "privkey{0}.pem".format(prior_version)) if os.path.islink(old_privkey): old_privkey = os.readlink(old_privkey) else: old_privkey = "privkey{0}.pem".format(prior_version) logger.debug("Writing symlink to old private key, %s.", old_privkey) os.symlink(old_privkey, target["privkey"]) else: with open(target["privkey"], "w") as f: logger.debug("Writing new private key to %s.", target["privkey"]) f.write(new_privkey) # Save everything else with open(target["cert"], "w") as f: logger.debug("Writing certificate to %s.", target["cert"]) f.write(new_cert) with open(target["chain"], "w") as f: logger.debug("Writing chain to %s.", target["chain"]) f.write(new_chain) with open(target["fullchain"], "w") as f: logger.debug("Writing full chain to %s.", target["fullchain"]) f.write(new_cert + new_chain) symlinks = dict((kind, self.configuration[kind]) for kind in ALL_FOUR) # Update renewal config file self.configfile = update_configuration( self.lineagename, symlinks, cli_config) self.configuration = config_with_defaults(self.configfile) return target_version letsencrypt-0.4.1/letsencrypt/reporter.py0000644000175000017500000000626312665157707020303 0ustar bmwbmw00000000000000"""Collects and displays information to the user.""" from __future__ import print_function import collections import logging import os import Queue import sys import textwrap import zope.interface from letsencrypt import interfaces from letsencrypt import le_util logger = logging.getLogger(__name__) @zope.interface.implementer(interfaces.IReporter) class Reporter(object): """Collects and displays information to the user. :ivar `Queue.PriorityQueue` messages: Messages to be displayed to the user. """ HIGH_PRIORITY = 0 """High priority constant. See `add_message`.""" MEDIUM_PRIORITY = 1 """Medium priority constant. See `add_message`.""" LOW_PRIORITY = 2 """Low priority constant. See `add_message`.""" _msg_type = collections.namedtuple('ReporterMsg', 'priority text on_crash') def __init__(self): self.messages = Queue.PriorityQueue() def add_message(self, msg, priority, on_crash=True): """Adds msg to the list of messages to be printed. :param str msg: Message to be displayed to the user. :param int priority: One of `HIGH_PRIORITY`, `MEDIUM_PRIORITY`, or `LOW_PRIORITY`. :param bool on_crash: Whether or not the message should be printed if the program exits abnormally. """ assert self.HIGH_PRIORITY <= priority <= self.LOW_PRIORITY self.messages.put(self._msg_type(priority, msg, on_crash)) logger.info("Reporting to user: %s", msg) def atexit_print_messages(self, pid=os.getpid()): """Function to be registered with atexit to print messages. :param int pid: Process ID """ # This ensures that messages are only printed from the process that # created the Reporter. if pid == os.getpid(): self.print_messages() def print_messages(self): """Prints messages to the user and clears the message queue. If there is an unhandled exception, only messages for which ``on_crash`` is ``True`` are printed. """ bold_on = False if not self.messages.empty(): no_exception = sys.exc_info()[0] is None bold_on = sys.stdout.isatty() if bold_on: print(le_util.ANSI_SGR_BOLD) print('IMPORTANT NOTES:') first_wrapper = textwrap.TextWrapper( initial_indent=' - ', subsequent_indent=(' ' * 3)) next_wrapper = textwrap.TextWrapper( initial_indent=first_wrapper.subsequent_indent, subsequent_indent=first_wrapper.subsequent_indent) while not self.messages.empty(): msg = self.messages.get() if no_exception or msg.on_crash: if bold_on and msg.priority > self.HIGH_PRIORITY: sys.stdout.write(le_util.ANSI_SGR_RESET) bold_on = False lines = msg.text.splitlines() print(first_wrapper.fill(lines[0])) if len(lines) > 1: print("\n".join( next_wrapper.fill(line) for line in lines[1:])) if bold_on: sys.stdout.write(le_util.ANSI_SGR_RESET) letsencrypt-0.4.1/letsencrypt/cli.py0000644000175000017500000025115312665157707017210 0ustar bmwbmw00000000000000"""Let's Encrypt CLI.""" from __future__ import print_function # TODO: Sanity check all input. Be sure to avoid shell code etc... # pylint: disable=too-many-lines # (TODO: split this file into main.py and cli.py) import argparse import atexit import copy import functools import glob import json import logging import logging.handlers import os import sys import time import traceback import configargparse import OpenSSL import zope.component import zope.interface.exceptions import zope.interface.verify from acme import jose import letsencrypt from letsencrypt import account from letsencrypt import colored_logging from letsencrypt import configuration from letsencrypt import constants from letsencrypt import client from letsencrypt import crypto_util from letsencrypt import errors from letsencrypt import interfaces from letsencrypt import le_util from letsencrypt import log from letsencrypt import reporter from letsencrypt import storage from letsencrypt.display import util as display_util from letsencrypt.display import ops as display_ops from letsencrypt.plugins import disco as plugins_disco logger = logging.getLogger(__name__) # Global, to save us from a lot of argument passing within the scope of this module _parser = None # These are the items which get pulled out of a renewal configuration # file's renewalparams and actually used in the client configuration # during the renewal process. We have to record their types here because # the renewal configuration process loses this information. STR_CONFIG_ITEMS = ["config_dir", "logs_dir", "work_dir", "user_agent", "server", "account", "authenticator", "installer", "standalone_supported_challenges"] INT_CONFIG_ITEMS = ["rsa_key_size", "tls_sni_01_port", "http01_port"] # For help strings, figure out how the user ran us. # When invoked from letsencrypt-auto, sys.argv[0] is something like: # "/home/user/.local/share/letsencrypt/bin/letsencrypt" # Note that this won't work if the user set VENV_PATH or XDG_DATA_HOME before running # letsencrypt-auto (and sudo stops us from seeing if they did), so it should only be used # for purposes where inability to detect letsencrypt-auto fails safely fragment = os.path.join(".local", "share", "letsencrypt") cli_command = "letsencrypt-auto" if fragment in sys.argv[0] else "letsencrypt" # Argparse's help formatting has a lot of unhelpful peculiarities, so we want # to replace as much of it as we can... # This is the stub to include in help generated by argparse SHORT_USAGE = """ {0} [SUBCOMMAND] [options] [-d domain] [-d domain] ... The Let's Encrypt agent can obtain and install HTTPS/TLS/SSL certificates. By default, it will attempt to use a webserver both for obtaining and installing the cert. Major SUBCOMMANDS are: (default) run Obtain & install a cert in your current webserver certonly Obtain cert, but do not install it (aka "auth") install Install a previously obtained cert in a server renew Renew previously obtained certs that are near expiry revoke Revoke a previously obtained certificate rollback Rollback server configuration changes made during install config_changes Show changes made to server config during installation plugins Display information about installed plugins """.format(cli_command) # This is the short help for letsencrypt --help, where we disable argparse # altogether USAGE = SHORT_USAGE + """Choice of server plugins for obtaining and installing cert: %s --standalone Run a standalone webserver for authentication %s --webroot Place files in a server's webroot folder for authentication OR use different plugins to obtain (authenticate) the cert and then install it: --authenticator standalone --installer apache More detailed help: -h, --help [topic] print this message, or detailed help on a topic; the available topics are: all, automation, paths, security, testing, or any of the subcommands or plugins (certonly, install, nginx, apache, standalone, webroot, etc) """ def usage_strings(plugins): """Make usage strings late so that plugins can be initialised late""" if "nginx" in plugins: nginx_doc = "--nginx Use the Nginx plugin for authentication & installation" else: nginx_doc = "(nginx support is experimental, buggy, and not installed by default)" if "apache" in plugins: apache_doc = "--apache Use the Apache plugin for authentication & installation" else: apache_doc = "(the apache plugin is not installed)" return USAGE % (apache_doc, nginx_doc), SHORT_USAGE def _find_domains(config, installer): if not config.domains: domains = display_ops.choose_names(installer) # record in config.domains (so that it can be serialised in renewal config files), # and set webroot_map entries if applicable for d in domains: _process_domain(config, d) else: domains = config.domains if not domains: raise errors.Error("Please specify --domains, or --installer that " "will help in domain names autodiscovery") return domains def _determine_account(config): """Determine which account to use. In order to make the renewer (configuration de/serialization) happy, if ``config.account`` is ``None``, it will be updated based on the user input. Same for ``config.email``. :param argparse.Namespace config: CLI arguments :param letsencrypt.interface.IConfig config: Configuration object :param .AccountStorage account_storage: Account storage. :returns: Account and optionally ACME client API (biproduct of new registration). :rtype: `tuple` of `letsencrypt.account.Account` and `acme.client.Client` """ account_storage = account.AccountFileStorage(config) acme = None if config.account is not None: acc = account_storage.load(config.account) else: accounts = account_storage.find_all() if len(accounts) > 1: acc = display_ops.choose_account(accounts) elif len(accounts) == 1: acc = accounts[0] else: # no account registered yet if config.email is None and not config.register_unsafely_without_email: config.namespace.email = display_ops.get_email() def _tos_cb(regr): if config.tos: return True msg = ("Please read the Terms of Service at {0}. You " "must agree in order to register with the ACME " "server at {1}".format( regr.terms_of_service, config.server)) obj = zope.component.getUtility(interfaces.IDisplay) return obj.yesno(msg, "Agree", "Cancel", cli_flag="--agree-tos") try: acc, acme = client.register( config, account_storage, tos_cb=_tos_cb) except errors.MissingCommandlineFlag: raise except errors.Error as error: logger.debug(error, exc_info=True) raise errors.Error( "Unable to register an account with ACME server") config.namespace.account = acc.id return acc, acme def _init_le_client(config, authenticator, installer): if authenticator is not None: # if authenticator was given, then we will need account... acc, acme = _determine_account(config) logger.debug("Picked account: %r", acc) # XXX #crypto_util.validate_key_csr(acc.key) else: acc, acme = None, None return client.Client(config, acc, authenticator, installer, acme=acme) def _find_duplicative_certs(config, domains): """Find existing certs that duplicate the request.""" identical_names_cert, subset_names_cert = None, None cli_config = configuration.RenewerConfiguration(config) configs_dir = cli_config.renewal_configs_dir # Verify the directory is there le_util.make_or_verify_dir(configs_dir, mode=0o755, uid=os.geteuid()) for renewal_file in _renewal_conf_files(cli_config): try: candidate_lineage = storage.RenewableCert(renewal_file, cli_config) except (errors.CertStorageError, IOError): logger.warning("Renewal conf file %s is broken. Skipping.", renewal_file) logger.debug("Traceback was:\n%s", traceback.format_exc()) continue # TODO: Handle these differently depending on whether they are # expired or still valid? candidate_names = set(candidate_lineage.names()) if candidate_names == set(domains): identical_names_cert = candidate_lineage elif candidate_names.issubset(set(domains)): # This logic finds and returns the largest subset-names cert # in the case where there are several available. if subset_names_cert is None: subset_names_cert = candidate_lineage elif len(candidate_names) > len(subset_names_cert.names()): subset_names_cert = candidate_lineage return identical_names_cert, subset_names_cert def _treat_as_renewal(config, domains): """Determine whether there are duplicated names and how to handle them (renew, reinstall, newcert, or raising an error to stop the client run if the user chooses to cancel the operation when prompted). :returns: Two-element tuple containing desired new-certificate behavior as a string token ("reinstall", "renew", or "newcert"), plus either a RenewableCert instance or None if renewal shouldn't occur. :raises .Error: If the user would like to rerun the client again. """ # Considering the possibility that the requested certificate is # related to an existing certificate. (config.duplicate, which # is set with --duplicate, skips all of this logic and forces any # kind of certificate to be obtained with renewal = False.) if config.duplicate: return "newcert", None # TODO: Also address superset case ident_names_cert, subset_names_cert = _find_duplicative_certs(config, domains) # XXX ^ schoen is not sure whether that correctly reads the systemwide # configuration file. if ident_names_cert is None and subset_names_cert is None: return "newcert", None if ident_names_cert is not None: return _handle_identical_cert_request(config, ident_names_cert) elif subset_names_cert is not None: return _handle_subset_cert_request(config, domains, subset_names_cert) def _should_renew(config, lineage): "Return true if any of the circumstances for automatic renewal apply." if config.renew_by_default: logger.info("Auto-renewal forced with --force-renewal...") return True if lineage.should_autorenew(interactive=True): logger.info("Cert is due for renewal, auto-renewing...") return True if config.dry_run: logger.info("Cert not due for renewal, but simulating renewal for dry run") return True logger.info("Cert not yet due for renewal") return False def _handle_identical_cert_request(config, cert): """Figure out what to do if a cert has the same names as a previously obtained one :param storage.RenewableCert cert: :returns: Tuple of (string, cert_or_None) as per _treat_as_renewal :rtype: tuple """ if _should_renew(config, cert): return "renew", cert if config.reinstall: # Set with --reinstall, force an identical certificate to be # reinstalled without further prompting. return "reinstall", cert question = ( "You have an existing certificate that contains exactly the same " "domains you requested and isn't close to expiry." "{br}(ref: {0}){br}{br}What would you like to do?" ).format(cert.configfile.filename, br=os.linesep) if config.verb == "run": keep_opt = "Attempt to reinstall this existing certificate" elif config.verb == "certonly": keep_opt = "Keep the existing certificate for now" choices = [keep_opt, "Renew & replace the cert (limit ~5 per 7 days)"] display = zope.component.getUtility(interfaces.IDisplay) response = display.menu(question, choices, "OK", "Cancel", default=0) if response[0] == display_util.CANCEL: # TODO: Add notification related to command-line options for # skipping the menu for this case. raise errors.Error( "User chose to cancel the operation and may " "reinvoke the client.") elif response[1] == 0: return "reinstall", cert elif response[1] == 1: return "renew", cert else: assert False, "This is impossible" def _handle_subset_cert_request(config, domains, cert): """Figure out what to do if a previous cert had a subset of the names now requested :param storage.RenewableCert cert: :returns: Tuple of (string, cert_or_None) as per _treat_as_renewal :rtype: tuple """ existing = ", ".join(cert.names()) question = ( "You have an existing certificate that contains a portion of " "the domains you requested (ref: {0}){br}{br}It contains these " "names: {1}{br}{br}You requested these names for the new " "certificate: {2}.{br}{br}Do you want to expand and replace this existing " "certificate with the new certificate?" ).format(cert.configfile.filename, existing, ", ".join(domains), br=os.linesep) if config.expand or config.renew_by_default or zope.component.getUtility( interfaces.IDisplay).yesno(question, "Expand", "Cancel", cli_flag="--expand (or in some cases, --duplicate)"): return "renew", cert else: reporter_util = zope.component.getUtility(interfaces.IReporter) reporter_util.add_message( "To obtain a new certificate that contains these names without " "replacing your existing certificate for {0}, you must use the " "--duplicate option.{br}{br}" "For example:{br}{br}{1} --duplicate {2}".format( existing, sys.argv[0], " ".join(sys.argv[1:]), br=os.linesep ), reporter_util.HIGH_PRIORITY) raise errors.Error( "User chose to cancel the operation and may " "reinvoke the client.") def _report_new_cert(cert_path, fullchain_path): """Reports the creation of a new certificate to the user. :param str cert_path: path to cert :param str fullchain_path: path to full chain """ expiry = crypto_util.notAfter(cert_path).date() reporter_util = zope.component.getUtility(interfaces.IReporter) if fullchain_path: # Print the path to fullchain.pem because that's what modern webservers # (Nginx and Apache2.4) will want. and_chain = "and chain have" path = fullchain_path else: # Unless we're in .csr mode and there really isn't one and_chain = "has " path = cert_path # XXX Perhaps one day we could detect the presence of known old webservers # and say something more informative here. msg = ("Congratulations! Your certificate {0} been saved at {1}." " Your cert will expire on {2}. To obtain a new version of the " "certificate in the future, simply run Let's Encrypt again." .format(and_chain, path, expiry)) reporter_util.add_message(msg, reporter_util.MEDIUM_PRIORITY) def _suggest_donation_if_appropriate(config, action): """Potentially suggest a donation to support Let's Encrypt.""" if config.staging or config.verb == "renew": # --dry-run implies --staging return if action not in ["renew", "newcert"]: return reporter_util = zope.component.getUtility(interfaces.IReporter) msg = ("If you like Let's Encrypt, please consider supporting our work by:\n\n" "Donating to ISRG / Let's Encrypt: https://letsencrypt.org/donate\n" "Donating to EFF: https://eff.org/donate-le\n\n") reporter_util.add_message(msg, reporter_util.LOW_PRIORITY) def _report_successful_dry_run(config): reporter_util = zope.component.getUtility(interfaces.IReporter) if config.verb != "renew": reporter_util.add_message("The dry run was successful.", reporter_util.HIGH_PRIORITY, on_crash=False) def _auth_from_domains(le_client, config, domains, lineage=None): """Authenticate and enroll certificate.""" # Note: This can raise errors... caught above us though. This is now # a three-way case: reinstall (which results in a no-op here because # although there is a relevant lineage, we don't do anything to it # inside this function -- we don't obtain a new certificate), renew # (which results in treating the request as a renewal), or newcert # (which results in treating the request as a new certificate request). # If lineage is specified, use that one instead of looking around for # a matching one. if lineage is None: # This will find a relevant matching lineage that exists action, lineage = _treat_as_renewal(config, domains) else: # Renewal, where we already know the specific lineage we're # interested in action = "renew" if action == "reinstall": # The lineage already exists; allow the caller to try installing # it without getting a new certificate at all. return lineage, "reinstall" elif action == "renew": original_server = lineage.configuration["renewalparams"]["server"] _avoid_invalidating_lineage(config, lineage, original_server) # TODO: schoen wishes to reuse key - discussion # https://github.com/letsencrypt/letsencrypt/pull/777/files#r40498574 new_certr, new_chain, new_key, _ = le_client.obtain_certificate(domains) # TODO: Check whether it worked! <- or make sure errors are thrown (jdk) if config.dry_run: logger.info("Dry run: skipping updating lineage at %s", os.path.dirname(lineage.cert)) else: lineage.save_successor( lineage.latest_common_version(), OpenSSL.crypto.dump_certificate( OpenSSL.crypto.FILETYPE_PEM, new_certr.body.wrapped), new_key.pem, crypto_util.dump_pyopenssl_chain(new_chain), configuration.RenewerConfiguration(config.namespace)) lineage.update_all_links_to(lineage.latest_common_version()) # TODO: Check return value of save_successor # TODO: Also update lineage renewal config with any relevant # configuration values from this attempt? <- Absolutely (jdkasten) elif action == "newcert": # TREAT AS NEW REQUEST lineage = le_client.obtain_and_enroll_certificate(domains) if lineage is False: raise errors.Error("Certificate could not be obtained") if not config.dry_run and not config.verb == "renew": _report_new_cert(lineage.cert, lineage.fullchain) return lineage, action def _avoid_invalidating_lineage(config, lineage, original_server): "Do not renew a valid cert with one from a staging server!" def _is_staging(srv): return srv == constants.STAGING_URI or "staging" in srv # Some lineages may have begun with --staging, but then had production certs # added to them latest_cert = OpenSSL.crypto.load_certificate(OpenSSL.crypto.FILETYPE_PEM, open(lineage.cert).read()) # all our test certs are from happy hacker fake CA, though maybe one day # we should test more methodically now_valid = "fake" not in repr(latest_cert.get_issuer()).lower() if _is_staging(config.server): if not _is_staging(original_server) or now_valid: if not config.break_my_certs: names = ", ".join(lineage.names()) raise errors.Error( "You've asked to renew/replace a seemingly valid certificate with " "a test certificate (domains: {0}). We will not do that " "unless you use the --break-my-certs flag!".format(names)) def diagnose_configurator_problem(cfg_type, requested, plugins): """ Raise the most helpful error message about a plugin being unavailable :param str cfg_type: either "installer" or "authenticator" :param str requested: the plugin that was requested :param .PluginsRegistry plugins: available plugins :raises error.PluginSelectionError: if there was a problem """ if requested: if requested not in plugins: msg = "The requested {0} plugin does not appear to be installed".format(requested) else: msg = ("The {0} plugin is not working; there may be problems with " "your existing configuration.\nThe error was: {1!r}" .format(requested, plugins[requested].problem)) elif cfg_type == "installer": if os.path.exists("/etc/debian_version"): # Debian... installers are at least possible msg = ('No installers seem to be present and working on your system; ' 'fix that or try running letsencrypt with the "certonly" command') else: # XXX update this logic as we make progress on #788 and nginx support msg = ('No installers are available on your OS yet; try running ' '"letsencrypt-auto certonly" to get a cert you can install manually') else: msg = "{0} could not be determined or is not installed".format(cfg_type) raise errors.PluginSelectionError(msg) def set_configurator(previously, now): """ Setting configurators multiple ways is okay, as long as they all agree :param str previously: previously identified request for the installer/authenticator :param str requested: the request currently being processed """ if now is None: # we're not actually setting anything return previously if previously: if previously != now: msg = "Too many flags setting configurators/installers/authenticators {0} -> {1}" raise errors.PluginSelectionError(msg.format(repr(previously), repr(now))) return now def cli_plugin_requests(config): """ Figure out which plugins the user requested with CLI and config options :returns: (requested authenticator string or None, requested installer string or None) :rtype: tuple """ req_inst = req_auth = config.configurator req_inst = set_configurator(req_inst, config.installer) req_auth = set_configurator(req_auth, config.authenticator) if config.nginx: req_inst = set_configurator(req_inst, "nginx") req_auth = set_configurator(req_auth, "nginx") if config.apache: req_inst = set_configurator(req_inst, "apache") req_auth = set_configurator(req_auth, "apache") if config.standalone: req_auth = set_configurator(req_auth, "standalone") if config.webroot: req_auth = set_configurator(req_auth, "webroot") if config.manual: req_auth = set_configurator(req_auth, "manual") logger.debug("Requested authenticator %s and installer %s", req_auth, req_inst) return req_auth, req_inst noninstaller_plugins = ["webroot", "manual", "standalone"] def choose_configurator_plugins(config, plugins, verb): """ Figure out which configurator we're going to use, modifies config.authenticator and config.istaller strings to reflect that choice if necessary. :raises errors.PluginSelectionError if there was a problem :returns: (an `IAuthenticator` or None, an `IInstaller` or None) :rtype: tuple """ req_auth, req_inst = cli_plugin_requests(config) # Which plugins do we need? if verb == "run": need_inst = need_auth = True if req_auth in noninstaller_plugins and not req_inst: msg = ('With the {0} plugin, you probably want to use the "certonly" command, eg:{1}' '{1} {2} certonly --{0}{1}{1}' '(Alternatively, add a --installer flag. See https://eff.org/letsencrypt-plugins' '{1} and "--help plugins" for more information.)'.format( req_auth, os.linesep, cli_command)) raise errors.MissingCommandlineFlag(msg) else: need_inst = need_auth = False if verb == "certonly": need_auth = True if verb == "install": need_inst = True if config.authenticator: logger.warn("Specifying an authenticator doesn't make sense in install mode") # Try to meet the user's request and/or ask them to pick plugins authenticator = installer = None if verb == "run" and req_auth == req_inst: # Unless the user has explicitly asked for different auth/install, # only consider offering a single choice authenticator = installer = display_ops.pick_configurator(config, req_inst, plugins) else: if need_inst or req_inst: installer = display_ops.pick_installer(config, req_inst, plugins) if need_auth: authenticator = display_ops.pick_authenticator(config, req_auth, plugins) logger.debug("Selected authenticator %s and installer %s", authenticator, installer) # Report on any failures if need_inst and not installer: diagnose_configurator_problem("installer", req_inst, plugins) if need_auth and not authenticator: diagnose_configurator_problem("authenticator", req_auth, plugins) record_chosen_plugins(config, plugins, authenticator, installer) return installer, authenticator def record_chosen_plugins(config, plugins, auth, inst): "Update the config entries to reflect the plugins we actually selected." cn = config.namespace cn.authenticator = plugins.find_init(auth).name if auth else "none" cn.installer = plugins.find_init(inst).name if inst else "none" # TODO: Make run as close to auth + install as possible # Possible difficulties: config.csr was hacked into auth def run(config, plugins): # pylint: disable=too-many-branches,too-many-locals """Obtain a certificate and install.""" try: installer, authenticator = choose_configurator_plugins(config, plugins, "run") except errors.PluginSelectionError as e: return e.message domains = _find_domains(config, installer) # TODO: Handle errors from _init_le_client? le_client = _init_le_client(config, authenticator, installer) lineage, action = _auth_from_domains(le_client, config, domains) le_client.deploy_certificate( domains, lineage.privkey, lineage.cert, lineage.chain, lineage.fullchain) le_client.enhance_config(domains, config) if len(lineage.available_versions("cert")) == 1: display_ops.success_installation(domains) else: display_ops.success_renewal(domains, action) _suggest_donation_if_appropriate(config, action) def obtain_cert(config, plugins, lineage=None): """Implements "certonly": authenticate & obtain cert, but do not install it.""" # pylint: disable=too-many-locals try: # installers are used in auth mode to determine domain names installer, authenticator = choose_configurator_plugins(config, plugins, "certonly") except errors.PluginSelectionError as e: logger.info("Could not choose appropriate plugin: %s", e) raise # TODO: Handle errors from _init_le_client? le_client = _init_le_client(config, authenticator, installer) action = "newcert" # This is a special case; cert and chain are simply saved if config.csr is not None: assert lineage is None, "Did not expect a CSR with a RenewableCert" csr, typ = config.actual_csr certr, chain = le_client.obtain_certificate_from_csr(config.domains, csr, typ) if config.dry_run: logger.info( "Dry run: skipping saving certificate to %s", config.cert_path) else: cert_path, _, cert_fullchain = le_client.save_certificate( certr, chain, config.cert_path, config.chain_path, config.fullchain_path) _report_new_cert(cert_path, cert_fullchain) else: domains = _find_domains(config, installer) _, action = _auth_from_domains(le_client, config, domains, lineage) if config.dry_run: _report_successful_dry_run(config) elif config.verb == "renew": if installer is None: # Tell the user that the server was not restarted. print("new certificate deployed without reload, fullchain is", lineage.fullchain) else: # In case of a renewal, reload server to pick up new certificate. # In principle we could have a configuration option to inhibit this # from happening. installer.restart() print("new certificate deployed with reload of", config.installer, "server; fullchain is", lineage.fullchain) _suggest_donation_if_appropriate(config, action) def install(config, plugins): """Install a previously obtained cert in a server.""" # XXX: Update for renewer/RenewableCert # FIXME: be consistent about whether errors are raised or returned from # this function ... try: installer, _ = choose_configurator_plugins(config, plugins, "install") except errors.PluginSelectionError as e: return e.message domains = _find_domains(config, installer) le_client = _init_le_client(config, authenticator=None, installer=installer) assert config.cert_path is not None # required=True in the subparser le_client.deploy_certificate( domains, config.key_path, config.cert_path, config.chain_path, config.fullchain_path) le_client.enhance_config(domains, config) def _set_by_cli(var): """ Return True if a particular config variable has been set by the user (CLI or config file) including if the user explicitly set it to the default. Returns False if the variable was assigned a default value. """ detector = _set_by_cli.detector if detector is None: # Setup on first run: `detector` is a weird version of config in which # the default value of every attribute is wrangled to be boolean-false plugins = plugins_disco.PluginsRegistry.find_all() # reconstructed_args == sys.argv[1:], or whatever was passed to main() reconstructed_args = _parser.args + [_parser.verb] detector = _set_by_cli.detector = prepare_and_parse_args( plugins, reconstructed_args, detect_defaults=True) # propagate plugin requests: eg --standalone modifies config.authenticator auth, inst = cli_plugin_requests(detector) detector.authenticator = auth if auth else "" detector.installer = inst if inst else "" logger.debug("Default Detector is %r", detector) try: # Is detector.var something that isn't false? change_detected = getattr(detector, var) except AttributeError: logger.warning("Missing default analysis for %r", var) return False if change_detected: return True # Special case: we actually want account to be set to "" if the server # the account was on has changed elif var == "account" and (detector.server or detector.dry_run or detector.staging): return True # Special case: vars like --no-redirect that get set True -> False # default to None; False means they were set elif var in detector.store_false_vars and change_detected is not None: return True else: return False # static housekeeping var _set_by_cli.detector = None def _restore_required_config_elements(config, renewalparams): """Sets non-plugin specific values in config from renewalparams :param configuration.NamespaceConfig config: configuration for the current lineage :param configobj.Section renewalparams: parameters from the renewal configuration file that defines this lineage """ # string-valued items to add if they're present for config_item in STR_CONFIG_ITEMS: if config_item in renewalparams and not _set_by_cli(config_item): value = renewalparams[config_item] # Unfortunately, we've lost type information from ConfigObj, # so we don't know if the original was NoneType or str! if value == "None": value = None setattr(config.namespace, config_item, value) # int-valued items to add if they're present for config_item in INT_CONFIG_ITEMS: if config_item in renewalparams and not _set_by_cli(config_item): try: value = int(renewalparams[config_item]) setattr(config.namespace, config_item, value) except ValueError: raise errors.Error( "Expected a numeric value for {0}".format(config_item)) def _restore_plugin_configs(config, renewalparams): """Sets plugin specific values in config from renewalparams :param configuration.NamespaceConfig config: configuration for the current lineage :param configobj.Section renewalparams: Parameters from the renewal configuration file that defines this lineage """ # Now use parser to get plugin-prefixed items with correct types # XXX: the current approach of extracting only prefixed items # related to the actually-used installer and authenticator # works as long as plugins don't need to read plugin-specific # variables set by someone else (e.g., assuming Apache # configurator doesn't need to read webroot_ variables). # Note: if a parameter that used to be defined in the parser is no # longer defined, stored copies of that parameter will be # deserialized as strings by this logic even if they were # originally meant to be some other type. if renewalparams["authenticator"] == "webroot": _restore_webroot_config(config, renewalparams) plugin_prefixes = [] else: plugin_prefixes = [renewalparams["authenticator"]] if renewalparams.get("installer", None) is not None: plugin_prefixes.append(renewalparams["installer"]) for plugin_prefix in set(plugin_prefixes): for config_item, config_value in renewalparams.iteritems(): if config_item.startswith(plugin_prefix + "_") and not _set_by_cli(config_item): # Values None, True, and False need to be treated specially, # As they don't get parsed correctly based on type if config_value in ("None", "True", "False"): # bool("False") == True # pylint: disable=eval-used setattr(config.namespace, config_item, eval(config_value)) continue for action in _parser.parser._actions: # pylint: disable=protected-access if action.type is not None and action.dest == config_item: setattr(config.namespace, config_item, action.type(config_value)) break else: setattr(config.namespace, config_item, str(config_value)) def _restore_webroot_config(config, renewalparams): """ webroot_map is, uniquely, a dict, and the general-purpose configuration restoring logic is not able to correctly parse it from the serialized form. """ if "webroot_map" in renewalparams: # if the user does anything that would create a new webroot map on the # CLI, don't use the old one if not (_set_by_cli("webroot_map") or _set_by_cli("webroot_path")): setattr(config.namespace, "webroot_map", renewalparams["webroot_map"]) elif "webroot_path" in renewalparams: logger.info("Ancient renewal conf file without webroot-map, restoring webroot-path") wp = renewalparams["webroot_path"] if isinstance(wp, str): # prior to 0.1.0, webroot_path was a string wp = [wp] setattr(config.namespace, "webroot_path", wp) def _reconstitute(config, full_path): """Try to instantiate a RenewableCert, updating config with relevant items. This is specifically for use in renewal and enforces several checks and policies to ensure that we can try to proceed with the renwal request. The config argument is modified by including relevant options read from the renewal configuration file. :param configuration.NamespaceConfig config: configuration for the current lineage :param str full_path: Absolute path to the configuration file that defines this lineage :returns: the RenewableCert object or None if a fatal error occurred :rtype: `storage.RenewableCert` or NoneType """ try: renewal_candidate = storage.RenewableCert( full_path, configuration.RenewerConfiguration(config)) except (errors.CertStorageError, IOError): logger.warning("Renewal configuration file %s is broken. Skipping.", full_path) logger.debug("Traceback was:\n%s", traceback.format_exc()) return None if "renewalparams" not in renewal_candidate.configuration: logger.warning("Renewal configuration file %s lacks " "renewalparams. Skipping.", full_path) return None renewalparams = renewal_candidate.configuration["renewalparams"] if "authenticator" not in renewalparams: logger.warning("Renewal configuration file %s does not specify " "an authenticator. Skipping.", full_path) return None # Now restore specific values along with their data types, if # those elements are present. try: _restore_required_config_elements(config, renewalparams) _restore_plugin_configs(config, renewalparams) except (ValueError, errors.Error) as error: logger.warning( "An error occured while parsing %s. The error was %s. " "Skipping the file.", full_path, error.message) logger.debug("Traceback was:\n%s", traceback.format_exc()) return None try: for d in renewal_candidate.names(): _process_domain(config, d) except errors.ConfigurationError as error: logger.warning("Renewal configuration file %s references a cert " "that contains an invalid domain name. The problem " "was: %s. Skipping.", full_path, error) return None return renewal_candidate def _renewal_conf_files(config): """Return /path/to/*.conf in the renewal conf directory""" return glob.glob(os.path.join(config.renewal_configs_dir, "*.conf")) def _renew_describe_results(config, renew_successes, renew_failures, renew_skipped, parse_failures): status = lambda x, msg: " " + "\n ".join(i + " (" + msg +")" for i in x) if config.dry_run: print("** DRY RUN: simulating 'letsencrypt renew' close to cert expiry") print("** (The test certificates below have not been saved.)") print() if renew_skipped: print("The following certs are not due for renewal yet:") print(status(renew_skipped, "skipped")) if not renew_successes and not renew_failures: print("No renewals were attempted.") elif renew_successes and not renew_failures: print("Congratulations, all renewals succeeded. The following certs " "have been renewed:") print(status(renew_successes, "success")) elif renew_failures and not renew_successes: print("All renewal attempts failed. The following certs could not be " "renewed:") print(status(renew_failures, "failure")) elif renew_failures and renew_successes: print("The following certs were successfully renewed:") print(status(renew_successes, "success")) print("\nThe following certs could not be renewed:") print(status(renew_failures, "failure")) if parse_failures: print("\nAdditionally, the following renewal configuration files " "were invalid: ") print(status(parse_failures, "parsefail")) if config.dry_run: print("** DRY RUN: simulating 'letsencrypt renew' close to cert expiry") print("** (The test certificates above have not been saved.)") def renew(config, unused_plugins): """Renew previously-obtained certificates.""" if config.domains != []: raise errors.Error("Currently, the renew verb is only capable of " "renewing all installed certificates that are due " "to be renewed; individual domains cannot be " "specified with this action. If you would like to " "renew specific certificates, use the certonly " "command. The renew verb may provide other options " "for selecting certificates to renew in the future.") renewer_config = configuration.RenewerConfiguration(config) renew_successes = [] renew_failures = [] renew_skipped = [] parse_failures = [] for renewal_file in _renewal_conf_files(renewer_config): print("Processing " + renewal_file) lineage_config = copy.deepcopy(config) # Note that this modifies config (to add back the configuration # elements from within the renewal configuration file). try: renewal_candidate = _reconstitute(lineage_config, renewal_file) except Exception as e: # pylint: disable=broad-except logger.warning("Renewal configuration file %s produced an " "unexpected error: %s. Skipping.", renewal_file, e) logger.debug("Traceback was:\n%s", traceback.format_exc()) parse_failures.append(renewal_file) continue try: if renewal_candidate is None: parse_failures.append(renewal_file) else: # XXX: ensure that each call here replaces the previous one zope.component.provideUtility(lineage_config) if _should_renew(lineage_config, renewal_candidate): plugins = plugins_disco.PluginsRegistry.find_all() obtain_cert(lineage_config, plugins, renewal_candidate) renew_successes.append(renewal_candidate.fullchain) else: renew_skipped.append(renewal_candidate.fullchain) except Exception as e: # pylint: disable=broad-except # obtain_cert (presumably) encountered an unanticipated problem. logger.warning("Attempting to renew cert from %s produced an " "unexpected error: %s. Skipping.", renewal_file, e) logger.debug("Traceback was:\n%s", traceback.format_exc()) renew_failures.append(renewal_candidate.fullchain) # Describe all the results _renew_describe_results(config, renew_successes, renew_failures, renew_skipped, parse_failures) if renew_failures or parse_failures: raise errors.Error("{0} renew failure(s), {1} parse failure(s)".format( len(renew_failures), len(parse_failures))) else: logger.debug("no renewal failures") def revoke(config, unused_plugins): # TODO: coop with renewal config """Revoke a previously obtained certificate.""" # For user-agent construction config.namespace.installer = config.namespace.authenticator = "none" if config.key_path is not None: # revocation by cert key logger.debug("Revoking %s using cert key %s", config.cert_path[0], config.key_path[0]) key = jose.JWK.load(config.key_path[1]) else: # revocation by account key logger.debug("Revoking %s using Account Key", config.cert_path[0]) acc, _ = _determine_account(config) key = acc.key acme = client.acme_from_config_key(config, key) cert = crypto_util.pyopenssl_load_certificate(config.cert_path[1])[0] acme.revoke(jose.ComparableX509(cert)) def rollback(config, plugins): """Rollback server configuration changes made during install.""" client.rollback(config.installer, config.checkpoints, config, plugins) def config_changes(config, unused_plugins): """Show changes made to server config during installation View checkpoints and associated configuration changes. """ client.view_config_changes(config) def plugins_cmd(config, plugins): # TODO: Use IDisplay rather than print """List server software plugins.""" logger.debug("Expected interfaces: %s", config.ifaces) ifaces = [] if config.ifaces is None else config.ifaces filtered = plugins.visible().ifaces(ifaces) logger.debug("Filtered plugins: %r", filtered) if not config.init and not config.prepare: print(str(filtered)) return filtered.init(config) verified = filtered.verify(ifaces) logger.debug("Verified plugins: %r", verified) if not config.prepare: print(str(verified)) return verified.prepare() available = verified.available() logger.debug("Prepared plugins: %s", available) print(str(available)) def read_file(filename, mode="rb"): """Returns the given file's contents. :param str filename: path to file :param str mode: open mode (see `open`) :returns: absolute path of filename and its contents :rtype: tuple :raises argparse.ArgumentTypeError: File does not exist or is not readable. """ try: filename = os.path.abspath(filename) return filename, open(filename, mode).read() except IOError as exc: raise argparse.ArgumentTypeError(exc.strerror) def flag_default(name): """Default value for CLI flag.""" return constants.CLI_DEFAULTS[name] def config_help(name, hidden=False): """Help message for `.IConfig` attribute.""" if hidden: return argparse.SUPPRESS else: return interfaces.IConfig[name].__doc__ class SilentParser(object): # pylint: disable=too-few-public-methods """Silent wrapper around argparse. A mini parser wrapper that doesn't print help for its arguments. This is needed for the use of callbacks to define arguments within plugins. """ def __init__(self, parser): self.parser = parser def add_argument(self, *args, **kwargs): """Wrap, but silence help""" kwargs["help"] = argparse.SUPPRESS self.parser.add_argument(*args, **kwargs) class HelpfulArgumentParser(object): """Argparse Wrapper. This class wraps argparse, adding the ability to make --help less verbose, and request help on specific subcategories at a time, eg 'letsencrypt --help security' for security options. """ # Maps verbs/subcommands to the functions that implement them VERBS = {"auth": obtain_cert, "certonly": obtain_cert, "config_changes": config_changes, "everything": run, "install": install, "plugins": plugins_cmd, "renew": renew, "revoke": revoke, "rollback": rollback, "run": run} # List of topics for which additional help can be provided HELP_TOPICS = ["all", "security", "paths", "automation", "testing"] + VERBS.keys() def __init__(self, args, plugins, detect_defaults=False): plugin_names = [name for name, _p in plugins.iteritems()] self.help_topics = self.HELP_TOPICS + plugin_names + [None] usage, short_usage = usage_strings(plugins) self.parser = configargparse.ArgParser( usage=short_usage, formatter_class=argparse.ArgumentDefaultsHelpFormatter, args_for_setting_config_path=["-c", "--config"], default_config_files=flag_default("config_files")) # This is the only way to turn off overly verbose config flag documentation self.parser._add_config_file_help = False # pylint: disable=protected-access self.silent_parser = SilentParser(self.parser) # This setting attempts to force all default values to things that are # pythonically false; it is used to detect when values have been # explicitly set by the user, including when they are set to their # normal default value self.detect_defaults = detect_defaults if detect_defaults: self.store_false_vars = {} # vars that use "store_false" self.args = args self.determine_verb() help1 = self.prescan_for_flag("-h", self.help_topics) help2 = self.prescan_for_flag("--help", self.help_topics) assert max(True, "a") == "a", "Gravity changed direction" self.help_arg = max(help1, help2) if self.help_arg is True: # just --help with no topic; avoid argparse altogether print(usage) sys.exit(0) self.visible_topics = self.determine_help_topics(self.help_arg) self.groups = {} # elements are added by .add_group() def parse_args(self): """Parses command line arguments and returns the result. :returns: parsed command line arguments :rtype: argparse.Namespace """ parsed_args = self.parser.parse_args(self.args) parsed_args.func = self.VERBS[self.verb] parsed_args.verb = self.verb # Do any post-parsing homework here # we get domains from -d, but also from the webroot map... if parsed_args.webroot_map: for domain in parsed_args.webroot_map.keys(): if domain not in parsed_args.domains: parsed_args.domains.append(domain) if parsed_args.staging or parsed_args.dry_run: if parsed_args.server not in (flag_default("server"), constants.STAGING_URI): conflicts = ["--staging"] if parsed_args.staging else [] conflicts += ["--dry-run"] if parsed_args.dry_run else [] if not self.detect_defaults: raise errors.Error("--server value conflicts with {0}".format( " and ".join(conflicts))) parsed_args.server = constants.STAGING_URI if parsed_args.dry_run: if self.verb not in ["certonly", "renew"]: raise errors.Error("--dry-run currently only works with the " "'certonly' or 'renew' subcommands (%r)" % self.verb) parsed_args.break_my_certs = parsed_args.staging = True if glob.glob(os.path.join(parsed_args.config_dir, constants.ACCOUNTS_DIR, "*")): # The user has a prod account, but might not have a staging # one; we don't want to start trying to perform interactive registration parsed_args.agree_tos = True parsed_args.register_unsafely_without_email = True if parsed_args.csr: self.handle_csr(parsed_args) if self.detect_defaults: # plumbing parsed_args.store_false_vars = self.store_false_vars return parsed_args def handle_csr(self, parsed_args): """ Process a --csr flag. This needs to happen early enough that the webroot plugin can know about the calls to _process_domain """ if parsed_args.verb != "certonly": raise errors.Error("Currently, a CSR file may only be specified " "when obtaining a new or replacement " "via the certonly command. Please try the " "certonly command instead.") try: csr = le_util.CSR(file=parsed_args.csr[0], data=parsed_args.csr[1], form="der") typ = OpenSSL.crypto.FILETYPE_ASN1 domains = crypto_util.get_sans_from_csr(csr.data, OpenSSL.crypto.FILETYPE_ASN1) except OpenSSL.crypto.Error: try: e1 = traceback.format_exc() typ = OpenSSL.crypto.FILETYPE_PEM csr = le_util.CSR(file=parsed_args.csr[0], data=parsed_args.csr[1], form="pem") domains = crypto_util.get_sans_from_csr(csr.data, typ) except OpenSSL.crypto.Error: logger.debug("DER CSR parse error %s", e1) logger.debug("PEM CSR parse error %s", traceback.format_exc()) raise errors.Error("Failed to parse CSR file: {0}".format(parsed_args.csr[0])) for d in domains: _process_domain(parsed_args, d) for d in domains: sanitised = le_util.enforce_domain_sanity(d) if d.lower() != sanitised: raise errors.ConfigurationError( "CSR domain {0} needs to be sanitised to {1}.".format(d, sanitised)) if not domains: # TODO: add CN to domains instead: raise errors.Error( "Unfortunately, your CSR %s needs to have a SubjectAltName for every domain" % parsed_args.csr[0]) parsed_args.actual_csr = (csr, typ) csr_domains, config_domains = set(domains), set(parsed_args.domains) if csr_domains != config_domains: raise errors.ConfigurationError( "Inconsistent domain requests:\nFrom the CSR: {0}\nFrom command line/config: {1}" .format(", ".join(csr_domains), ", ".join(config_domains))) def determine_verb(self): """Determines the verb/subcommand provided by the user. This function works around some of the limitations of argparse. """ if "-h" in self.args or "--help" in self.args: # all verbs double as help arguments; don't get them confused self.verb = "help" return for i, token in enumerate(self.args): if token in self.VERBS: verb = token if verb == "auth": verb = "certonly" if verb == "everything": verb = "run" self.verb = verb self.args.pop(i) return self.verb = "run" def prescan_for_flag(self, flag, possible_arguments): """Checks cli input for flags. Check for a flag, which accepts a fixed set of possible arguments, in the command line; we will use this information to configure argparse's help correctly. Return the flag's argument, if it has one that matches the sequence @possible_arguments; otherwise return whether the flag is present. """ if flag not in self.args: return False pos = self.args.index(flag) try: nxt = self.args[pos + 1] if nxt in possible_arguments: return nxt except IndexError: pass return True def add(self, topic, *args, **kwargs): """Add a new command line argument. :param str: help topic this should be listed under, can be None for "always documented" :param list *args: the names of this argument flag :param dict **kwargs: various argparse settings for this argument """ if self.detect_defaults: kwargs = self.modify_arg_for_default_detection(self, *args, **kwargs) if self.visible_topics[topic]: if topic in self.groups: group = self.groups[topic] group.add_argument(*args, **kwargs) else: self.parser.add_argument(*args, **kwargs) else: kwargs["help"] = argparse.SUPPRESS self.parser.add_argument(*args, **kwargs) def modify_arg_for_default_detection(self, *args, **kwargs): """ Adding an arg, but ensure that it has a default that evaluates to false, so that _set_by_cli can tell if it was set. Only called if detect_defaults==True. :param list *args: the names of this argument flag :param dict **kwargs: various argparse settings for this argument :returns: a modified versions of kwargs """ # argument either doesn't have a default, or the default doesn't # isn't Pythonically false if kwargs.get("default", True): arg_type = kwargs.get("type", None) if arg_type == int or kwargs.get("action", "") == "count": kwargs["default"] = 0 elif arg_type == read_file or "-c" in args: kwargs["default"] = "" kwargs["type"] = str else: kwargs["default"] = "" # This doesn't matter at present (none of the store_false args # are renewal-relevant), but implement it for future sanity: # detect the setting of args whose presence causes True -> False if kwargs.get("action", "") == "store_false": kwargs["default"] = None for var in args: self.store_false_vars[var] = True return kwargs def add_deprecated_argument(self, argument_name, num_args): """Adds a deprecated argument with the name argument_name. Deprecated arguments are not shown in the help. If they are used on the command line, a warning is shown stating that the argument is deprecated and no other action is taken. :param str argument_name: Name of deprecated argument. :param int nargs: Number of arguments the option takes. """ le_util.add_deprecated_argument( self.parser.add_argument, argument_name, num_args) def add_group(self, topic, **kwargs): """ This has to be called once for every topic; but we leave those calls next to the argument definitions for clarity. Return something arguments can be added to if necessary, either the parser or an argument group. """ if self.visible_topics[topic]: #print("Adding visible group " + topic) group = self.parser.add_argument_group(topic, **kwargs) self.groups[topic] = group return group else: #print("Invisible group " + topic) return self.silent_parser def add_plugin_args(self, plugins): """ Let each of the plugins add its own command line arguments, which may or may not be displayed as help topics. """ for name, plugin_ep in plugins.iteritems(): parser_or_group = self.add_group(name, description=plugin_ep.description) #print(parser_or_group) plugin_ep.plugin_cls.inject_parser_options(parser_or_group, name) def determine_help_topics(self, chosen_topic): """ The user may have requested help on a topic, return a dict of which topics to display. @chosen_topic has prescan_for_flag's return type :returns: dict """ # topics maps each topic to whether it should be documented by # argparse on the command line if chosen_topic == "auth": chosen_topic = "certonly" if chosen_topic == "everything": chosen_topic = "run" if chosen_topic == "all": return dict([(t, True) for t in self.help_topics]) elif not chosen_topic: return dict([(t, False) for t in self.help_topics]) else: return dict([(t, t == chosen_topic) for t in self.help_topics]) def prepare_and_parse_args(plugins, args, detect_defaults=False): """Returns parsed command line arguments. :param .PluginsRegistry plugins: available plugins :param list args: command line arguments with the program name removed :returns: parsed command line arguments :rtype: argparse.Namespace """ helpful = HelpfulArgumentParser(args, plugins, detect_defaults) # --help is automatically provided by argparse helpful.add( None, "-v", "--verbose", dest="verbose_count", action="count", default=flag_default("verbose_count"), help="This flag can be used " "multiple times to incrementally increase the verbosity of output, " "e.g. -vvv.") helpful.add( None, "-t", "--text", dest="text_mode", action="store_true", help="Use the text output instead of the curses UI.") helpful.add( None, "-n", "--non-interactive", "--noninteractive", dest="noninteractive_mode", action="store_true", help="Run without ever asking for user input. This may require " "additional command line flags; the client will try to explain " "which ones are required if it finds one missing") helpful.add( None, "--dry-run", action="store_true", dest="dry_run", help="Perform a test run of the client, obtaining test (invalid) certs" " but not saving them to disk. This can currently only be used" " with the 'certonly' subcommand.") helpful.add( None, "--register-unsafely-without-email", action="store_true", help="Specifying this flag enables registering an account with no " "email address. This is strongly discouraged, because in the " "event of key loss or account compromise you will irrevocably " "lose access to your account. You will also be unable to receive " "notice about impending expiration or revocation of your " "certificates. Updates to the Subscriber Agreement will still " "affect you, and will be effective 14 days after posting an " "update to the web site.") helpful.add(None, "-m", "--email", help=config_help("email")) # positional arg shadows --domains, instead of appending, and # --domains is useful, because it can be stored in config #for subparser in parser_run, parser_auth, parser_install: # subparser.add_argument("domains", nargs="*", metavar="domain") helpful.add(None, "-d", "--domains", "--domain", dest="domains", metavar="DOMAIN", action=DomainFlagProcessor, default=[], help="Domain names to apply. For multiple domains you can use " "multiple -d flags or enter a comma separated list of domains " "as a parameter.") helpful.add_group( "automation", description="Arguments for automating execution & other tweaks") helpful.add( "automation", "--keep-until-expiring", "--keep", "--reinstall", dest="reinstall", action="store_true", help="If the requested cert matches an existing cert, always keep the " "existing one until it is due for renewal (for the " "'run' subcommand this means reinstall the existing cert)") helpful.add( "automation", "--expand", action="store_true", help="If an existing cert covers some subset of the requested names, " "always expand and replace it with the additional names.") helpful.add( "automation", "--version", action="version", version="%(prog)s {0}".format(letsencrypt.__version__), help="show program's version number and exit") helpful.add( "automation", "--force-renewal", "--renew-by-default", action="store_true", dest="renew_by_default", help="If a certificate " "already exists for the requested domains, renew it now, " "regardless of whether it is near expiry. (Often " "--keep-until-expiring is more appropriate). Also implies " "--expand.") helpful.add( "automation", "--agree-tos", dest="tos", action="store_true", help="Agree to the Let's Encrypt Subscriber Agreement") helpful.add( "automation", "--account", metavar="ACCOUNT_ID", help="Account ID to use") helpful.add( "automation", "--duplicate", dest="duplicate", action="store_true", help="Allow making a certificate lineage that duplicates an existing one " "(both can be renewed in parallel)") helpful.add( "automation", "--os-packages-only", action="store_true", help="(letsencrypt-auto only) install OS package dependencies and then stop") helpful.add( "automation", "--no-self-upgrade", action="store_true", help="(letsencrypt-auto only) prevent the letsencrypt-auto script from" " upgrading itself to newer released versions") helpful.add_group( "testing", description="The following flags are meant for " "testing purposes only! Do NOT change them, unless you " "really know what you're doing!") helpful.add( "testing", "--debug", action="store_true", help="Show tracebacks in case of errors, and allow letsencrypt-auto " "execution on experimental platforms") helpful.add( "testing", "--no-verify-ssl", action="store_true", help=config_help("no_verify_ssl"), default=flag_default("no_verify_ssl")) helpful.add( "testing", "--tls-sni-01-port", type=int, default=flag_default("tls_sni_01_port"), help=config_help("tls_sni_01_port")) helpful.add( "testing", "--http-01-port", type=int, dest="http01_port", default=flag_default("http01_port"), help=config_help("http01_port")) helpful.add( "testing", "--break-my-certs", action="store_true", help="Be willing to replace or renew valid certs with invalid " "(testing/staging) certs") helpful.add_group( "security", description="Security parameters & server settings") helpful.add( "security", "--rsa-key-size", type=int, metavar="N", default=flag_default("rsa_key_size"), help=config_help("rsa_key_size")) helpful.add( "security", "--redirect", action="store_true", help="Automatically redirect all HTTP traffic to HTTPS for the newly " "authenticated vhost.", dest="redirect", default=None) helpful.add( "security", "--no-redirect", action="store_false", help="Do not automatically redirect all HTTP traffic to HTTPS for the newly " "authenticated vhost.", dest="redirect", default=None) helpful.add( "security", "--hsts", action="store_true", help="Add the Strict-Transport-Security header to every HTTP response." " Forcing browser to use always use SSL for the domain." " Defends against SSL Stripping.", dest="hsts", default=False) helpful.add( "security", "--no-hsts", action="store_false", help="Do not automatically add the Strict-Transport-Security header" " to every HTTP response.", dest="hsts", default=False) helpful.add( "security", "--uir", action="store_true", help="Add the \"Content-Security-Policy: upgrade-insecure-requests\"" " header to every HTTP response. Forcing the browser to use" " https:// for every http:// resource.", dest="uir", default=None) helpful.add( "security", "--no-uir", action="store_false", help=" Do not automatically set the \"Content-Security-Policy:" " upgrade-insecure-requests\" header to every HTTP response.", dest="uir", default=None) helpful.add( "security", "--strict-permissions", action="store_true", help="Require that all configuration files are owned by the current " "user; only needed if your config is somewhere unsafe like /tmp/") helpful.add_group( "renew", description="The 'renew' subcommand will attempt to renew all" " certificates (or more precisely, certificate lineages) you have" " previously obtained if they are close to expiry, and print a" " summary of the results. By default, 'renew' will reuse the options" " used to create obtain or most recently successfully renew each" " certificate lineage. You can try it with `--dry-run` first. For" " more fine-grained control, you can renew individual lineages with" " the `certonly` subcommand.") helpful.add_deprecated_argument("--agree-dev-preview", 0) _create_subparsers(helpful) _paths_parser(helpful) # _plugins_parsing should be the last thing to act upon the main # parser (--help should display plugin-specific options last) _plugins_parsing(helpful, plugins) if not detect_defaults: global _parser # pylint: disable=global-statement _parser = helpful return helpful.parse_args() def _create_subparsers(helpful): helpful.add_group("certonly", description="Options for modifying how a cert is obtained") helpful.add_group("install", description="Options for modifying how a cert is deployed") helpful.add_group("revoke", description="Options for revocation of certs") helpful.add_group("rollback", description="Options for reverting config changes") helpful.add_group("plugins", description="Plugin options") helpful.add( None, "--user-agent", default=None, help="Set a custom user agent string for the client. User agent strings allow " "the CA to collect high level statistics about success rates by OS and " "plugin. If you wish to hide your server OS version from the Let's " 'Encrypt server, set this to "".') helpful.add("certonly", "--csr", type=read_file, help="Path to a Certificate Signing Request (CSR) in DER" " format; note that the .csr file *must* contain a Subject" " Alternative Name field for each domain you want certified." " Currently --csr only works with the 'certonly' subcommand'") helpful.add("rollback", "--checkpoints", type=int, metavar="N", default=flag_default("rollback_checkpoints"), help="Revert configuration N number of checkpoints.") helpful.add("plugins", "--init", action="store_true", help="Initialize plugins.") helpful.add("plugins", "--prepare", action="store_true", help="Initialize and prepare plugins.") helpful.add("plugins", "--authenticators", action="append_const", dest="ifaces", const=interfaces.IAuthenticator, help="Limit to authenticator plugins only.") helpful.add("plugins", "--installers", action="append_const", dest="ifaces", const=interfaces.IInstaller, help="Limit to installer plugins only.") def _paths_parser(helpful): add = helpful.add verb = helpful.verb if verb == "help": verb = helpful.help_arg helpful.add_group( "paths", description="Arguments changing execution paths & servers") cph = "Path to where cert is saved (with auth --csr), installed from or revoked." section = "paths" if verb in ("install", "revoke", "certonly"): section = verb if verb == "certonly": add(section, "--cert-path", type=os.path.abspath, default=flag_default("auth_cert_path"), help=cph) elif verb == "revoke": add(section, "--cert-path", type=read_file, required=True, help=cph) else: add(section, "--cert-path", type=os.path.abspath, help=cph, required=(verb == "install")) section = "paths" if verb in ("install", "revoke"): section = verb # revoke --key-path reads a file, install --key-path takes a string add(section, "--key-path", required=(verb == "install"), type=((verb == "revoke" and read_file) or os.path.abspath), help="Path to private key for cert installation " "or revocation (if account key is missing)") default_cp = None if verb == "certonly": default_cp = flag_default("auth_chain_path") add("paths", "--fullchain-path", default=default_cp, type=os.path.abspath, help="Accompanying path to a full certificate chain (cert plus chain).") add("paths", "--chain-path", default=default_cp, type=os.path.abspath, help="Accompanying path to a certificate chain.") add("paths", "--config-dir", default=flag_default("config_dir"), help=config_help("config_dir")) add("paths", "--work-dir", default=flag_default("work_dir"), help=config_help("work_dir")) add("paths", "--logs-dir", default=flag_default("logs_dir"), help="Logs directory.") add("paths", "--server", default=flag_default("server"), help=config_help("server")) # overwrites server, handled in HelpfulArgumentParser.parse_args() add("testing", "--test-cert", "--staging", action='store_true', dest='staging', help='Use the staging server to obtain test (invalid) certs; equivalent' ' to --server ' + constants.STAGING_URI) def _plugins_parsing(helpful, plugins): helpful.add_group( "plugins", description="Let's Encrypt client supports an " "extensible plugins architecture. See '%(prog)s plugins' for a " "list of all installed plugins and their names. You can force " "a particular plugin by setting options provided below. Running " "--help will list flags specific to that plugin.") helpful.add( "plugins", "-a", "--authenticator", help="Authenticator plugin name.") helpful.add( "plugins", "-i", "--installer", help="Installer plugin name (also used to find domains).") helpful.add( "plugins", "--configurator", help="Name of the plugin that is " "both an authenticator and an installer. Should not be used " "together with --authenticator or --installer.") helpful.add("plugins", "--apache", action="store_true", help="Obtain and install certs using Apache") helpful.add("plugins", "--nginx", action="store_true", help="Obtain and install certs using Nginx") helpful.add("plugins", "--standalone", action="store_true", help='Obtain certs using a "standalone" webserver.') helpful.add("plugins", "--manual", action="store_true", help='Provide laborious manual instructions for obtaining a cert') helpful.add("plugins", "--webroot", action="store_true", help='Obtain certs by placing files in a webroot directory.') # things should not be reorder past/pre this comment: # plugins_group should be displayed in --help before plugin # specific groups (so that plugins_group.description makes sense) helpful.add_plugin_args(plugins) # These would normally be a flag within the webroot plugin, but because # they are parsed in conjunction with --domains, they live here for # legibility. helpful.add_plugin_ags must be called first to add the # "webroot" topic helpful.add("webroot", "-w", "--webroot-path", default=[], action=WebrootPathProcessor, help="public_html / webroot path. This can be specified multiple times to " "handle different domains; each domain will have the webroot path that" " preceded it. For instance: `-w /var/www/example -d example.com -d " "www.example.com -w /var/www/thing -d thing.net -d m.thing.net`") # --webroot-map still has some awkward properties, so it is undocumented helpful.add("webroot", "--webroot-map", default={}, action=WebrootMapProcessor, help="JSON dictionary mapping domains to webroot paths; this " "implies -d for each entry. You may need to escape this " "from your shell. E.g.: --webroot-map " """'{"eg1.is,m.eg1.is":"/www/eg1/", "eg2.is":"/www/eg2"}' """ "This option is merged with, but takes precedence over, " "-w / -d entries. At present, if you put webroot-map in " "a config file, it needs to be on a single line, like: " 'webroot-map = {"example.com":"/var/www"}.') class WebrootPathProcessor(argparse.Action): # pylint: disable=missing-docstring def __init__(self, *args, **kwargs): self.domain_before_webroot = False argparse.Action.__init__(self, *args, **kwargs) def __call__(self, parser, args, webroot, option_string=None): """ Keep a record of --webroot-path / -w flags during processing, so that we know which apply to which -d flags """ if not args.webroot_path: # first -w flag encountered # if any --domain flags preceded the first --webroot-path flag, # apply that webroot path to those; subsequent entries in # args.webroot_map are filled in by cli.DomainFlagProcessor if args.domains: self.domain_before_webroot = True for d in args.domains: args.webroot_map.setdefault(d, webroot) elif self.domain_before_webroot: # FIXME if you set domains in a args file, you should get a different error # here, pointing you to --webroot-map raise errors.Error("If you specify multiple webroot paths, one of " "them must precede all domain flags") args.webroot_path.append(webroot) def _process_domain(args_or_config, domain_arg, webroot_path=None): """ Process a new -d flag, helping the webroot plugin construct a map of {domain : webrootpath} if -w / --webroot-path is in use :param args_or_config: may be an argparse args object, or a NamespaceConfig object :param str domain_arg: a string representing 1+ domains, eg: "eg.is, example.com" :param str webroot_path: (optional) the webroot_path for these domains """ webroot_path = webroot_path if webroot_path else args_or_config.webroot_path for domain in (d.strip() for d in domain_arg.split(",")): domain = le_util.enforce_domain_sanity(domain) if domain not in args_or_config.domains: args_or_config.domains.append(domain) # Each domain has a webroot_path of the most recent -w flag # unless it was explicitly included in webroot_map if webroot_path: args_or_config.webroot_map.setdefault(domain, webroot_path[-1]) class WebrootMapProcessor(argparse.Action): # pylint: disable=missing-docstring def __call__(self, parser, args, webroot_map_arg, option_string=None): webroot_map = json.loads(webroot_map_arg) for domains, webroot_path in webroot_map.iteritems(): _process_domain(args, domains, [webroot_path]) class DomainFlagProcessor(argparse.Action): # pylint: disable=missing-docstring def __call__(self, parser, args, domain_arg, option_string=None): """Just wrap _process_domain in argparseese.""" _process_domain(args, domain_arg) def setup_log_file_handler(config, logfile, fmt): """Setup file debug logging.""" log_file_path = os.path.join(config.logs_dir, logfile) handler = logging.handlers.RotatingFileHandler( log_file_path, maxBytes=2 ** 20, backupCount=10) # rotate on each invocation, rollover only possible when maxBytes # is nonzero and backupCount is nonzero, so we set maxBytes as big # as possible not to overrun in single CLI invocation (1MB). handler.doRollover() # TODO: creates empty letsencrypt.log.1 file handler.setLevel(logging.DEBUG) handler_formatter = logging.Formatter(fmt=fmt) handler_formatter.converter = time.gmtime # don't use localtime handler.setFormatter(handler_formatter) return handler, log_file_path def _cli_log_handler(config, level, fmt): if config.text_mode or config.noninteractive_mode or config.verb == "renew": handler = colored_logging.StreamHandler() handler.setFormatter(logging.Formatter(fmt)) else: handler = log.DialogHandler() # dialog box is small, display as less as possible handler.setFormatter(logging.Formatter("%(message)s")) handler.setLevel(level) return handler def setup_logging(config, cli_handler_factory, logfile): """Setup logging.""" fmt = "%(asctime)s:%(levelname)s:%(name)s:%(message)s" level = -config.verbose_count * 10 file_handler, log_file_path = setup_log_file_handler( config, logfile=logfile, fmt=fmt) cli_handler = cli_handler_factory(config, level, fmt) # TODO: use fileConfig? root_logger = logging.getLogger() root_logger.setLevel(logging.DEBUG) # send all records to handlers root_logger.addHandler(cli_handler) root_logger.addHandler(file_handler) logger.debug("Root logging level set at %d", level) logger.info("Saving debug log to %s", log_file_path) def _handle_exception(exc_type, exc_value, trace, config): """Logs exceptions and reports them to the user. Config is used to determine how to display exceptions to the user. In general, if config.debug is True, then the full exception and traceback is shown to the user, otherwise it is suppressed. If config itself is None, then the traceback and exception is attempted to be written to a logfile. If this is successful, the traceback is suppressed, otherwise it is shown to the user. sys.exit is always called with a nonzero status. """ logger.debug( "Exiting abnormally:%s%s", os.linesep, "".join(traceback.format_exception(exc_type, exc_value, trace))) if issubclass(exc_type, Exception) and (config is None or not config.debug): if config is None: logfile = "letsencrypt.log" try: with open(logfile, "w") as logfd: traceback.print_exception( exc_type, exc_value, trace, file=logfd) except: # pylint: disable=bare-except sys.exit("".join( traceback.format_exception(exc_type, exc_value, trace))) if issubclass(exc_type, errors.Error): sys.exit(exc_value) else: # Here we're passing a client or ACME error out to the client at the shell # Tell the user a bit about what happened, without overwhelming # them with a full traceback err = traceback.format_exception_only(exc_type, exc_value)[0] # Typical error from the ACME module: # acme.messages.Error: urn:acme:error:malformed :: The request message was # malformed :: Error creating new registration :: Validation of contact # mailto:none@longrandomstring.biz failed: Server failure at resolver if (("urn:acme" in err and ":: " in err and config.verbose_count <= flag_default("verbose_count"))): # prune ACME error code, we have a human description _code, _sep, err = err.partition(":: ") msg = "An unexpected error occurred:\n" + err + "Please see the " if config is None: msg += "logfile '{0}' for more details.".format(logfile) else: msg += "logfiles in {0} for more details.".format(config.logs_dir) sys.exit(msg) else: sys.exit("".join( traceback.format_exception(exc_type, exc_value, trace))) def main(cli_args=sys.argv[1:]): """Command line argument parsing and main script execution.""" sys.excepthook = functools.partial(_handle_exception, config=None) plugins = plugins_disco.PluginsRegistry.find_all() # note: arg parser internally handles --help (and exits afterwards) args = prepare_and_parse_args(plugins, cli_args) config = configuration.NamespaceConfig(args) zope.component.provideUtility(config) # Setup logging ASAP, otherwise "No handlers could be found for # logger ..." TODO: this should be done before plugins discovery for directory in config.config_dir, config.work_dir: le_util.make_or_verify_dir( directory, constants.CONFIG_DIRS_MODE, os.geteuid(), "--strict-permissions" in cli_args) # TODO: logs might contain sensitive data such as contents of the # private key! #525 le_util.make_or_verify_dir( config.logs_dir, 0o700, os.geteuid(), "--strict-permissions" in cli_args) setup_logging(config, _cli_log_handler, logfile='letsencrypt.log') logger.debug("letsencrypt version: %s", letsencrypt.__version__) # do not log `config`, as it contains sensitive data (e.g. revoke --key)! logger.debug("Arguments: %r", cli_args) logger.debug("Discovered plugins: %r", plugins) sys.excepthook = functools.partial(_handle_exception, config=config) # Displayer if config.noninteractive_mode: displayer = display_util.NoninteractiveDisplay(sys.stdout) elif config.text_mode: displayer = display_util.FileDisplay(sys.stdout) elif config.verb == "renew": config.noninteractive_mode = True displayer = display_util.NoninteractiveDisplay(sys.stdout) else: displayer = display_util.NcursesDisplay() zope.component.provideUtility(displayer) # Reporter report = reporter.Reporter() zope.component.provideUtility(report) atexit.register(report.atexit_print_messages) return config.func(config, plugins) if __name__ == "__main__": err_string = main() if err_string: logger.warn("Exiting with message %s", err_string) sys.exit(err_string) # pragma: no cover letsencrypt-0.4.1/letsencrypt/constants.py0000644000175000017500000000523712665157707020455 0ustar bmwbmw00000000000000"""Let's Encrypt constants.""" import os import logging from acme import challenges SETUPTOOLS_PLUGINS_ENTRY_POINT = "letsencrypt.plugins" """Setuptools entry point group name for plugins.""" CLI_DEFAULTS = dict( config_files=[ "/etc/letsencrypt/cli.ini", # http://freedesktop.org/wiki/Software/xdg-user-dirs/ os.path.join(os.environ.get("XDG_CONFIG_HOME", "~/.config"), "letsencrypt", "cli.ini"), ], verbose_count=-(logging.WARNING / 10), server="https://acme-v01.api.letsencrypt.org/directory", rsa_key_size=2048, rollback_checkpoints=1, config_dir="/etc/letsencrypt", work_dir="/var/lib/letsencrypt", logs_dir="/var/log/letsencrypt", no_verify_ssl=False, http01_port=challenges.HTTP01Response.PORT, tls_sni_01_port=challenges.TLSSNI01Response.PORT, auth_cert_path="./cert.pem", auth_chain_path="./chain.pem", strict_permissions=False, ) STAGING_URI = "https://acme-staging.api.letsencrypt.org/directory" """Defaults for CLI flags and `.IConfig` attributes.""" RENEWER_DEFAULTS = dict( renewer_enabled="yes", renew_before_expiry="30 days", # This value should ensure that there is never a deployment delay by # default. deploy_before_expiry="99 years", ) """Defaults for renewer script.""" EXCLUSIVE_CHALLENGES = frozenset([frozenset([ challenges.TLSSNI01, challenges.HTTP01])]) """Mutually exclusive challenges.""" ENHANCEMENTS = ["redirect", "http-header", "ocsp-stapling", "spdy"] """List of possible :class:`letsencrypt.interfaces.IInstaller` enhancements. List of expected options parameters: - redirect: None - http-header: TODO - ocsp-stapling: TODO - spdy: TODO """ ARCHIVE_DIR = "archive" """Archive directory, relative to `IConfig.config_dir`.""" CONFIG_DIRS_MODE = 0o755 """Directory mode for ``.IConfig.config_dir`` et al.""" ACCOUNTS_DIR = "accounts" """Directory where all accounts are saved.""" BACKUP_DIR = "backups" """Directory (relative to `IConfig.work_dir`) where backups are kept.""" CSR_DIR = "csr" """See `.IConfig.csr_dir`.""" IN_PROGRESS_DIR = "IN_PROGRESS" """Directory used before a permanent checkpoint is finalized (relative to `IConfig.work_dir`).""" KEY_DIR = "keys" """Directory (relative to `IConfig.config_dir`) where keys are saved.""" LIVE_DIR = "live" """Live directory, relative to `IConfig.config_dir`.""" TEMP_CHECKPOINT_DIR = "temp_checkpoint" """Temporary checkpoint directory (relative to `IConfig.work_dir`).""" RENEWAL_CONFIGS_DIR = "renewal" """Renewal configs directory, relative to `IConfig.config_dir`.""" RENEWER_CONFIG_FILENAME = "renewer.conf" """Renewer config file name (relative to `IConfig.config_dir`).""" letsencrypt-0.4.1/letsencrypt/proof_of_possession.py0000644000175000017500000000707312665157707022537 0ustar bmwbmw00000000000000"""Proof of Possession Identifier Validation Challenge.""" import logging import os from cryptography import x509 from cryptography.hazmat.backends import default_backend import zope.component from acme import challenges from acme import jose from acme import other from letsencrypt import interfaces from letsencrypt.display import util as display_util logger = logging.getLogger(__name__) class ProofOfPossession(object): # pylint: disable=too-few-public-methods """Proof of Possession Identifier Validation Challenge. Based on draft-barnes-acme, section 6.5. :ivar installer: Installer object :type installer: :class:`~letsencrypt.interfaces.IInstaller` """ def __init__(self, installer): self.installer = installer def perform(self, achall): """Perform the Proof of Possession Challenge. :param achall: Proof of Possession Challenge :type achall: :class:`letsencrypt.achallenges.ProofOfPossession` :returns: Response or None/False if the challenge cannot be completed :rtype: :class:`acme.challenges.ProofOfPossessionResponse` or False """ if (achall.alg in [jose.HS256, jose.HS384, jose.HS512] or not isinstance(achall.hints.jwk, achall.alg.kty)): return None for cert, key, _ in self.installer.get_all_certs_keys(): with open(cert) as cert_file: cert_data = cert_file.read() try: cert_obj = x509.load_pem_x509_certificate( cert_data, default_backend()) except ValueError: try: cert_obj = x509.load_der_x509_certificate( cert_data, default_backend()) except ValueError: logger.warn("Certificate is neither PER nor DER: %s", cert) cert_key = achall.alg.kty(key=cert_obj.public_key()) if cert_key == achall.hints.jwk: return self._gen_response(achall, key) # Is there are different prompt we should give the user? code, key = zope.component.getUtility( interfaces.IDisplay).input( "Path to private key for identifier: %s " % achall.domain) if code != display_util.CANCEL: return self._gen_response(achall, key) # If we get here, the key wasn't found return False def _gen_response(self, achall, key_path): # pylint: disable=no-self-use """Create the response to the Proof of Possession Challenge. :param achall: Proof of Possession Challenge :type achall: :class:`letsencrypt.achallenges.ProofOfPossession` :param str key_path: Path to the key corresponding to the hinted to public key. :returns: Response or False if the challenge cannot be completed :rtype: :class:`acme.challenges.ProofOfPossessionResponse` or False """ if os.path.isfile(key_path): with open(key_path, 'rb') as key: try: # Needs to be changed if JWKES doesn't have a key attribute jwk = achall.alg.kty.load(key.read()) sig = other.Signature.from_msg(achall.nonce, jwk.key, alg=achall.alg) except (IndexError, ValueError, TypeError, jose.errors.Error): return False return challenges.ProofOfPossessionResponse(nonce=achall.nonce, signature=sig) return False letsencrypt-0.4.1/letsencrypt/continuity_auth.py0000644000175000017500000000362312665157707021664 0ustar bmwbmw00000000000000"""Continuity Authenticator""" import zope.interface from acme import challenges from letsencrypt import achallenges from letsencrypt import errors from letsencrypt import interfaces from letsencrypt import proof_of_possession @zope.interface.implementer(interfaces.IAuthenticator) class ContinuityAuthenticator(object): """IAuthenticator for :const:`~acme.challenges.ContinuityChallenge` class challenges. :ivar proof_of_pos: Performs "proofOfPossession" challenges. :type proof_of_pos: :class:`letsencrypt.proof_of_possession.Proof_of_Possession` """ # This will have an installer soon for get_key/cert purposes def __init__(self, config, installer): # pylint: disable=unused-argument """Initialize Client Authenticator. :param config: Configuration. :type config: :class:`letsencrypt.interfaces.IConfig` :param installer: Let's Encrypt Installer. :type installer: :class:`letsencrypt.interfaces.IInstaller` """ self.proof_of_pos = proof_of_possession.ProofOfPossession(installer) def get_chall_pref(self, unused_domain): # pylint: disable=no-self-use """Return list of challenge preferences.""" return [challenges.ProofOfPossession] def perform(self, achalls): """Perform client specific challenges for IAuthenticator""" responses = [] for achall in achalls: if isinstance(achall, achallenges.ProofOfPossession): responses.append(self.proof_of_pos.perform(achall)) else: raise errors.ContAuthError("Unexpected Challenge") return responses def cleanup(self, achalls): # pylint: disable=no-self-use """Cleanup call for IAuthenticator.""" for achall in achalls: if not isinstance(achall, achallenges.ProofOfPossession): raise errors.ContAuthError("Unexpected Challenge") letsencrypt-0.4.1/letsencrypt/plugins/0000755000175000017500000000000012665157717017542 5ustar bmwbmw00000000000000letsencrypt-0.4.1/letsencrypt/plugins/manual.py0000644000175000017500000001752112665157707021376 0ustar bmwbmw00000000000000"""Manual plugin.""" import os import logging import pipes import shutil import signal import socket import subprocess import sys import tempfile import time import zope.component import zope.interface from acme import challenges from letsencrypt import errors from letsencrypt import interfaces from letsencrypt.plugins import common logger = logging.getLogger(__name__) @zope.interface.implementer(interfaces.IAuthenticator) @zope.interface.provider(interfaces.IPluginFactory) class Authenticator(common.Plugin): """Manual Authenticator. This plugin requires user's manual intervention in setting up a HTTP server for solving http-01 challenges and thus does not need to be run as a privileged process. Alternatively shows instructions on how to use Python's built-in HTTP server. .. todo:: Support for `~.challenges.TLSSNI01`. """ hidden = True description = "Manually configure an HTTP server" MESSAGE_TEMPLATE = """\ Make sure your web server displays the following content at {uri} before continuing: {validation} If you don't have HTTP server configured, you can run the following command on the target server (as root): {command} """ # a disclaimer about your current IP being transmitted to Let's Encrypt's servers. IP_DISCLAIMER = """\ NOTE: The IP of this machine will be publicly logged as having requested this certificate. \ If you're running letsencrypt in manual mode on a machine that is not your server, \ please ensure you're okay with that. Are you OK with your IP being logged? """ # "cd /tmp/letsencrypt" makes sure user doesn't serve /root, # separate "public_html" ensures that cert.pem/key.pem are not # served and makes it more obvious that Python command will serve # anything recursively under the cwd CMD_TEMPLATE = """\ mkdir -p {root}/public_html/{achall.URI_ROOT_PATH} cd {root}/public_html printf "%s" {validation} > {achall.URI_ROOT_PATH}/{encoded_token} # run only once per server: $(command -v python2 || command -v python2.7 || command -v python2.6) -c \\ "import BaseHTTPServer, SimpleHTTPServer; \\ s = BaseHTTPServer.HTTPServer(('', {port}), SimpleHTTPServer.SimpleHTTPRequestHandler); \\ s.serve_forever()" """ """Command template.""" def __init__(self, *args, **kwargs): super(Authenticator, self).__init__(*args, **kwargs) self._root = (tempfile.mkdtemp() if self.conf("test-mode") else "/tmp/letsencrypt") self._httpd = None @classmethod def add_parser_arguments(cls, add): add("test-mode", action="store_true", help="Test mode. Executes the manual command in subprocess.") add("public-ip-logging-ok", action="store_true", help="Automatically allows public IP logging.") def prepare(self): # pylint: disable=missing-docstring,no-self-use if self.config.noninteractive_mode and not self.conf("test-mode"): raise errors.PluginError("Running manual mode non-interactively is not supported") def more_info(self): # pylint: disable=missing-docstring,no-self-use return ("This plugin requires user's manual intervention in setting " "up an HTTP server for solving http-01 challenges and thus " "does not need to be run as a privileged process. " "Alternatively shows instructions on how to use Python's " "built-in HTTP server.") def get_chall_pref(self, domain): # pylint: disable=missing-docstring,no-self-use,unused-argument return [challenges.HTTP01] def perform(self, achalls): # pylint: disable=missing-docstring responses = [] # TODO: group achalls by the same socket.gethostbyname(_ex) # and prompt only once per server (one "echo -n" per domain) for achall in achalls: responses.append(self._perform_single(achall)) return responses @classmethod def _test_mode_busy_wait(cls, port): while True: time.sleep(1) sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) try: sock.connect(("localhost", port)) except socket.error: # pragma: no cover pass else: break finally: sock.close() def _perform_single(self, achall): # same path for each challenge response would be easier for # users, but will not work if multiple domains point at the # same server: default command doesn't support virtual hosts response, validation = achall.response_and_validation() port = (response.port if self.config.http01_port is None else int(self.config.http01_port)) command = self.CMD_TEMPLATE.format( root=self._root, achall=achall, response=response, # TODO(kuba): pipes still necessary? validation=pipes.quote(validation), encoded_token=achall.chall.encode("token"), port=port) if self.conf("test-mode"): logger.debug("Test mode. Executing the manual command: %s", command) # sh shipped with OS X does't support echo -n, but supports printf try: self._httpd = subprocess.Popen( command, # don't care about setting stdout and stderr, # we're in test mode anyway shell=True, executable=None, # "preexec_fn" is UNIX specific, but so is "command" preexec_fn=os.setsid) except OSError as error: # ValueError should not happen! logger.debug( "Couldn't execute manual command: %s", error, exc_info=True) return False logger.debug("Manual command running as PID %s.", self._httpd.pid) # give it some time to bootstrap, before we try to verify # (cert generation in case of simpleHttpS might take time) self._test_mode_busy_wait(port) if self._httpd.poll() is not None: raise errors.Error("Couldn't execute manual command") else: if not self.conf("public-ip-logging-ok"): if not zope.component.getUtility(interfaces.IDisplay).yesno( self.IP_DISCLAIMER, "Yes", "No", cli_flag="--manual-public-ip-logging-ok"): raise errors.PluginError("Must agree to IP logging to proceed") self._notify_and_wait(self.MESSAGE_TEMPLATE.format( validation=validation, response=response, uri=achall.chall.uri(achall.domain), command=command)) if not response.simple_verify( achall.chall, achall.domain, achall.account_key.public_key(), self.config.http01_port): logger.warning("Self-verify of challenge failed.") return response def _notify_and_wait(self, message): # pylint: disable=no-self-use # TODO: IDisplay wraps messages, breaking the command #answer = zope.component.getUtility(interfaces.IDisplay).notification( # message=message, height=25, pause=True) sys.stdout.write(message) raw_input("Press ENTER to continue") def cleanup(self, achalls): # pylint: disable=missing-docstring,no-self-use,unused-argument if self.conf("test-mode"): assert self._httpd is not None, ( "cleanup() must be called after perform()") if self._httpd.poll() is None: logger.debug("Terminating manual command process") os.killpg(self._httpd.pid, signal.SIGTERM) else: logger.debug("Manual command process already terminated " "with %s code", self._httpd.returncode) shutil.rmtree(self._root) letsencrypt-0.4.1/letsencrypt/plugins/util_test.py0000644000175000017500000001252212665157707022131 0ustar bmwbmw00000000000000"""Tests for letsencrypt.plugins.util.""" import unittest import mock import psutil class AlreadyListeningTest(unittest.TestCase): """Tests for letsencrypt.plugins.already_listening.""" def _call(self, *args, **kwargs): from letsencrypt.plugins.util import already_listening return already_listening(*args, **kwargs) @mock.patch("letsencrypt.plugins.util.psutil.net_connections") @mock.patch("letsencrypt.plugins.util.psutil.Process") @mock.patch("letsencrypt.plugins.util.zope.component.getUtility") def test_race_condition(self, mock_get_utility, mock_process, mock_net): # This tests a race condition, or permission problem, or OS # incompatibility in which, for some reason, no process name can be # found to match the identified listening PID. from psutil._common import sconn conns = [ sconn(fd=-1, family=2, type=1, laddr=("0.0.0.0", 30), raddr=(), status="LISTEN", pid=None), sconn(fd=3, family=2, type=1, laddr=("192.168.5.10", 32783), raddr=("20.40.60.80", 22), status="ESTABLISHED", pid=1234), sconn(fd=-1, family=10, type=1, laddr=("::1", 54321), raddr=("::1", 111), status="CLOSE_WAIT", pid=None), sconn(fd=3, family=2, type=1, laddr=("0.0.0.0", 17), raddr=(), status="LISTEN", pid=4416)] mock_net.return_value = conns mock_process.side_effect = psutil.NoSuchProcess("No such PID") # We simulate being unable to find the process name of PID 4416, # which results in returning False. self.assertFalse(self._call(17)) self.assertEqual(mock_get_utility.generic_notification.call_count, 0) mock_process.assert_called_once_with(4416) @mock.patch("letsencrypt.plugins.util.psutil.net_connections") @mock.patch("letsencrypt.plugins.util.psutil.Process") @mock.patch("letsencrypt.plugins.util.zope.component.getUtility") def test_not_listening(self, mock_get_utility, mock_process, mock_net): from psutil._common import sconn conns = [ sconn(fd=-1, family=2, type=1, laddr=("0.0.0.0", 30), raddr=(), status="LISTEN", pid=None), sconn(fd=3, family=2, type=1, laddr=("192.168.5.10", 32783), raddr=("20.40.60.80", 22), status="ESTABLISHED", pid=1234), sconn(fd=-1, family=10, type=1, laddr=("::1", 54321), raddr=("::1", 111), status="CLOSE_WAIT", pid=None)] mock_net.return_value = conns mock_process.name.return_value = "inetd" self.assertFalse(self._call(17)) self.assertEqual(mock_get_utility.generic_notification.call_count, 0) self.assertEqual(mock_process.call_count, 0) @mock.patch("letsencrypt.plugins.util.psutil.net_connections") @mock.patch("letsencrypt.plugins.util.psutil.Process") @mock.patch("letsencrypt.plugins.util.zope.component.getUtility") def test_listening_ipv4(self, mock_get_utility, mock_process, mock_net): from psutil._common import sconn conns = [ sconn(fd=-1, family=2, type=1, laddr=("0.0.0.0", 30), raddr=(), status="LISTEN", pid=None), sconn(fd=3, family=2, type=1, laddr=("192.168.5.10", 32783), raddr=("20.40.60.80", 22), status="ESTABLISHED", pid=1234), sconn(fd=-1, family=10, type=1, laddr=("::1", 54321), raddr=("::1", 111), status="CLOSE_WAIT", pid=None), sconn(fd=3, family=2, type=1, laddr=("0.0.0.0", 17), raddr=(), status="LISTEN", pid=4416)] mock_net.return_value = conns mock_process.name.return_value = "inetd" result = self._call(17) self.assertTrue(result) self.assertEqual(mock_get_utility.call_count, 1) mock_process.assert_called_once_with(4416) @mock.patch("letsencrypt.plugins.util.psutil.net_connections") @mock.patch("letsencrypt.plugins.util.psutil.Process") @mock.patch("letsencrypt.plugins.util.zope.component.getUtility") def test_listening_ipv6(self, mock_get_utility, mock_process, mock_net): from psutil._common import sconn conns = [ sconn(fd=-1, family=2, type=1, laddr=("0.0.0.0", 30), raddr=(), status="LISTEN", pid=None), sconn(fd=3, family=2, type=1, laddr=("192.168.5.10", 32783), raddr=("20.40.60.80", 22), status="ESTABLISHED", pid=1234), sconn(fd=-1, family=10, type=1, laddr=("::1", 54321), raddr=("::1", 111), status="CLOSE_WAIT", pid=None), sconn(fd=3, family=10, type=1, laddr=("::", 12345), raddr=(), status="LISTEN", pid=4420), sconn(fd=3, family=2, type=1, laddr=("0.0.0.0", 17), raddr=(), status="LISTEN", pid=4416)] mock_net.return_value = conns mock_process.name.return_value = "inetd" result = self._call(12345) self.assertTrue(result) self.assertEqual(mock_get_utility.call_count, 1) mock_process.assert_called_once_with(4420) @mock.patch("letsencrypt.plugins.util.psutil.net_connections") def test_access_denied_exception(self, mock_net): mock_net.side_effect = psutil.AccessDenied("") self.assertFalse(self._call(12345)) if __name__ == "__main__": unittest.main() # pragma: no cover letsencrypt-0.4.1/letsencrypt/plugins/disco_test.py0000644000175000017500000002344112665157707022257 0ustar bmwbmw00000000000000"""Tests for letsencrypt.plugins.disco.""" import unittest import mock import pkg_resources import zope.interface from letsencrypt import errors from letsencrypt import interfaces from letsencrypt.plugins import standalone EP_SA = pkg_resources.EntryPoint( "sa", "letsencrypt.plugins.standalone", attrs=("Authenticator",), dist=mock.MagicMock(key="letsencrypt")) class PluginEntryPointTest(unittest.TestCase): """Tests for letsencrypt.plugins.disco.PluginEntryPoint.""" def setUp(self): self.ep1 = pkg_resources.EntryPoint( "ep1", "p1.ep1", dist=mock.MagicMock(key="p1")) self.ep1prim = pkg_resources.EntryPoint( "ep1", "p2.ep2", dist=mock.MagicMock(key="p2")) # nested self.ep2 = pkg_resources.EntryPoint( "ep2", "p2.foo.ep2", dist=mock.MagicMock(key="p2")) # project name != top-level package name self.ep3 = pkg_resources.EntryPoint( "ep3", "a.ep3", dist=mock.MagicMock(key="p3")) from letsencrypt.plugins.disco import PluginEntryPoint self.plugin_ep = PluginEntryPoint(EP_SA) def test_entry_point_to_plugin_name(self): from letsencrypt.plugins.disco import PluginEntryPoint names = { self.ep1: "p1:ep1", self.ep1prim: "p2:ep1", self.ep2: "p2:ep2", self.ep3: "p3:ep3", EP_SA: "sa", } for entry_point, name in names.iteritems(): self.assertEqual( name, PluginEntryPoint.entry_point_to_plugin_name(entry_point)) def test_description(self): self.assertEqual( "Automatically use a temporary webserver", self.plugin_ep.description) def test_description_with_name(self): self.plugin_ep.plugin_cls = mock.MagicMock(description="Desc") self.assertEqual( "Desc (sa)", self.plugin_ep.description_with_name) def test_ifaces(self): self.assertTrue(self.plugin_ep.ifaces((interfaces.IAuthenticator,))) self.assertFalse(self.plugin_ep.ifaces((interfaces.IInstaller,))) self.assertFalse(self.plugin_ep.ifaces(( interfaces.IInstaller, interfaces.IAuthenticator))) def test__init__(self): self.assertFalse(self.plugin_ep.initialized) self.assertFalse(self.plugin_ep.prepared) self.assertFalse(self.plugin_ep.misconfigured) self.assertFalse(self.plugin_ep.available) self.assertTrue(self.plugin_ep.problem is None) self.assertTrue(self.plugin_ep.entry_point is EP_SA) self.assertEqual("sa", self.plugin_ep.name) self.assertTrue(self.plugin_ep.plugin_cls is standalone.Authenticator) def test_init(self): config = mock.MagicMock() plugin = self.plugin_ep.init(config=config) self.assertTrue(self.plugin_ep.initialized) self.assertTrue(plugin.config is config) # memoize! self.assertTrue(self.plugin_ep.init() is plugin) self.assertTrue(plugin.config is config) # try to give different config self.assertTrue(self.plugin_ep.init(123) is plugin) self.assertTrue(plugin.config is config) self.assertFalse(self.plugin_ep.prepared) self.assertFalse(self.plugin_ep.misconfigured) self.assertFalse(self.plugin_ep.available) def test_verify(self): iface1 = mock.MagicMock(__name__="iface1") iface2 = mock.MagicMock(__name__="iface2") iface3 = mock.MagicMock(__name__="iface3") # pylint: disable=protected-access self.plugin_ep._initialized = plugin = mock.MagicMock() exceptions = zope.interface.exceptions with mock.patch("letsencrypt.plugins." "disco.zope.interface") as mock_zope: mock_zope.exceptions = exceptions def verify_object(iface, obj): # pylint: disable=missing-docstring assert obj is plugin assert iface is iface1 or iface is iface2 or iface is iface3 if iface is iface3: raise mock_zope.exceptions.BrokenImplementation(None, None) mock_zope.verify.verifyObject.side_effect = verify_object self.assertTrue(self.plugin_ep.verify((iface1,))) self.assertTrue(self.plugin_ep.verify((iface1, iface2))) self.assertFalse(self.plugin_ep.verify((iface3,))) self.assertFalse(self.plugin_ep.verify((iface1, iface3))) def test_prepare(self): config = mock.MagicMock() self.plugin_ep.init(config=config) self.plugin_ep.prepare() self.assertTrue(self.plugin_ep.prepared) self.assertFalse(self.plugin_ep.misconfigured) # output doesn't matter that much, just test if it runs str(self.plugin_ep) def test_prepare_misconfigured(self): plugin = mock.MagicMock() plugin.prepare.side_effect = errors.MisconfigurationError # pylint: disable=protected-access self.plugin_ep._initialized = plugin self.assertTrue(isinstance(self.plugin_ep.prepare(), errors.MisconfigurationError)) self.assertTrue(self.plugin_ep.prepared) self.assertTrue(self.plugin_ep.misconfigured) self.assertTrue(isinstance(self.plugin_ep.problem, errors.MisconfigurationError)) self.assertTrue(self.plugin_ep.available) def test_prepare_no_installation(self): plugin = mock.MagicMock() plugin.prepare.side_effect = errors.NoInstallationError # pylint: disable=protected-access self.plugin_ep._initialized = plugin self.assertTrue(isinstance(self.plugin_ep.prepare(), errors.NoInstallationError)) self.assertTrue(self.plugin_ep.prepared) self.assertFalse(self.plugin_ep.misconfigured) self.assertFalse(self.plugin_ep.available) def test_prepare_generic_plugin_error(self): plugin = mock.MagicMock() plugin.prepare.side_effect = errors.PluginError # pylint: disable=protected-access self.plugin_ep._initialized = plugin self.assertTrue(isinstance(self.plugin_ep.prepare(), errors.PluginError)) self.assertTrue(self.plugin_ep.prepared) self.assertFalse(self.plugin_ep.misconfigured) self.assertFalse(self.plugin_ep.available) def test_repr(self): self.assertEqual("PluginEntryPoint#sa", repr(self.plugin_ep)) class PluginsRegistryTest(unittest.TestCase): """Tests for letsencrypt.plugins.disco.PluginsRegistry.""" def setUp(self): from letsencrypt.plugins.disco import PluginsRegistry self.plugin_ep = mock.MagicMock(name="mock") self.plugin_ep.__hash__.side_effect = TypeError self.plugins = {"mock": self.plugin_ep} self.reg = PluginsRegistry(self.plugins) def test_find_all(self): from letsencrypt.plugins.disco import PluginsRegistry with mock.patch("letsencrypt.plugins.disco.pkg_resources") as mock_pkg: mock_pkg.iter_entry_points.return_value = iter([EP_SA]) plugins = PluginsRegistry.find_all() self.assertTrue(plugins["sa"].plugin_cls is standalone.Authenticator) self.assertTrue(plugins["sa"].entry_point is EP_SA) def test_getitem(self): self.assertEqual(self.plugin_ep, self.reg["mock"]) def test_iter(self): self.assertEqual(["mock"], list(self.reg)) def test_len(self): self.assertEqual(1, len(self.reg)) self.plugins.clear() self.assertEqual(0, len(self.reg)) def test_init(self): self.plugin_ep.init.return_value = "baz" self.assertEqual(["baz"], self.reg.init("bar")) self.plugin_ep.init.assert_called_once_with("bar") def test_filter(self): self.plugins.update({ "foo": "bar", "bar": "foo", "baz": "boo", }) self.assertEqual( {"foo": "bar", "baz": "boo"}, self.reg.filter(lambda p_ep: str(p_ep).startswith("b"))) def test_ifaces(self): self.plugin_ep.ifaces.return_value = True # pylint: disable=protected-access self.assertEqual(self.plugins, self.reg.ifaces()._plugins) self.plugin_ep.ifaces.return_value = False self.assertEqual({}, self.reg.ifaces()._plugins) def test_verify(self): self.plugin_ep.verify.return_value = True # pylint: disable=protected-access self.assertEqual( self.plugins, self.reg.verify(mock.MagicMock())._plugins) self.plugin_ep.verify.return_value = False self.assertEqual({}, self.reg.verify(mock.MagicMock())._plugins) def test_prepare(self): self.plugin_ep.prepare.return_value = "baz" self.assertEqual(["baz"], self.reg.prepare()) self.plugin_ep.prepare.assert_called_once_with() def test_available(self): self.plugin_ep.available = True # pylint: disable=protected-access self.assertEqual(self.plugins, self.reg.available()._plugins) self.plugin_ep.available = False self.assertEqual({}, self.reg.available()._plugins) def test_find_init(self): self.assertTrue(self.reg.find_init(mock.Mock()) is None) self.plugin_ep.initalized = True self.assertTrue( self.reg.find_init(self.plugin_ep.init()) is self.plugin_ep) def test_repr(self): self.plugin_ep.__repr__ = lambda _: "PluginEntryPoint#mock" self.assertEqual("PluginsRegistry(PluginEntryPoint#mock)", repr(self.reg)) def test_str(self): self.plugin_ep.__str__ = lambda _: "Mock" self.plugins["foo"] = "Mock" self.assertEqual("Mock\n\nMock", str(self.reg)) self.plugins.clear() self.assertEqual("No plugins", str(self.reg)) if __name__ == "__main__": unittest.main() # pragma: no cover letsencrypt-0.4.1/letsencrypt/plugins/webroot_test.py0000644000175000017500000001423512665157707022640 0ustar bmwbmw00000000000000"""Tests for letsencrypt.plugins.webroot.""" import errno import os import shutil import stat import tempfile import unittest import mock from acme import challenges from acme import jose from letsencrypt import achallenges from letsencrypt import errors from letsencrypt.tests import acme_util from letsencrypt.tests import test_util KEY = jose.JWKRSA.load(test_util.load_vector("rsa512_key.pem")) class AuthenticatorTest(unittest.TestCase): """Tests for letsencrypt.plugins.webroot.Authenticator.""" achall = achallenges.KeyAuthorizationAnnotatedChallenge( challb=acme_util.HTTP01_P, domain="thing.com", account_key=KEY) def setUp(self): from letsencrypt.plugins.webroot import Authenticator self.path = tempfile.mkdtemp() self.root_challenge_path = os.path.join( self.path, ".well-known", "acme-challenge") self.validation_path = os.path.join( self.root_challenge_path, "ZXZhR3hmQURzNnBTUmIyTEF2OUlaZjE3RHQzanV4R0orUEN0OTJ3citvQQ") self.config = mock.MagicMock(webroot_path=self.path, webroot_map={"thing.com": self.path}) self.auth = Authenticator(self.config, "webroot") def tearDown(self): shutil.rmtree(self.path) def test_more_info(self): more_info = self.auth.more_info() self.assertTrue(isinstance(more_info, str)) self.assertTrue(self.path in more_info) def test_add_parser_arguments(self): add = mock.MagicMock() self.auth.add_parser_arguments(add) self.assertEqual(0, add.call_count) # args moved to cli.py! def test_prepare_bad_root(self): self.config.webroot_path = os.path.join(self.path, "null") self.config.webroot_map["thing.com"] = self.config.webroot_path self.assertRaises(errors.PluginError, self.auth.prepare) def test_prepare_missing_root(self): self.config.webroot_path = None self.config.webroot_map = {} self.assertRaises(errors.PluginError, self.auth.prepare) def test_prepare_full_root_exists(self): # prepare() has already been called once in setUp() self.auth.prepare() # shouldn't raise any exceptions def test_prepare_reraises_other_errors(self): self.auth.full_path = os.path.join(self.path, "null") permission_canary = os.path.join(self.path, "rnd") with open(permission_canary, "w") as f: f.write("thingimy") os.chmod(self.path, 0o000) try: open(permission_canary, "r") print "Warning, running tests as root skips permissions tests..." except IOError: # ok, permissions work, test away... self.assertRaises(errors.PluginError, self.auth.prepare) os.chmod(self.path, 0o700) @mock.patch("letsencrypt.plugins.webroot.os.chown") def test_failed_chown_eacces(self, mock_chown): mock_chown.side_effect = OSError(errno.EACCES, "msg") self.auth.prepare() # exception caught and logged @mock.patch("letsencrypt.plugins.webroot.os.chown") def test_failed_chown_not_eacces(self, mock_chown): mock_chown.side_effect = OSError() self.assertRaises(errors.PluginError, self.auth.prepare) def test_prepare_permissions(self): self.auth.prepare() # Remove exec bit from permission check, so that it # matches the file self.auth.perform([self.achall]) path_permissions = stat.S_IMODE(os.stat(self.validation_path).st_mode) self.assertEqual(path_permissions, 0o644) # Check permissions of the directories for dirpath, dirnames, _ in os.walk(self.path): for directory in dirnames: full_path = os.path.join(dirpath, directory) dir_permissions = stat.S_IMODE(os.stat(full_path).st_mode) self.assertEqual(dir_permissions, 0o755) parent_gid = os.stat(self.path).st_gid parent_uid = os.stat(self.path).st_uid self.assertEqual(os.stat(self.validation_path).st_gid, parent_gid) self.assertEqual(os.stat(self.validation_path).st_uid, parent_uid) def test_perform_missing_path(self): self.auth.prepare() missing_achall = achallenges.KeyAuthorizationAnnotatedChallenge( challb=acme_util.HTTP01_P, domain="thing2.com", account_key=KEY) self.assertRaises( errors.PluginError, self.auth.perform, [missing_achall]) self.auth.full_roots[self.achall.domain] = 'null' self.assertRaises( errors.PluginError, self.auth.perform, [self.achall]) def test_perform_cleanup(self): self.auth.prepare() responses = self.auth.perform([self.achall]) self.assertEqual(1, len(responses)) self.assertTrue(os.path.exists(self.validation_path)) with open(self.validation_path) as validation_f: validation = validation_f.read() self.assertTrue( challenges.KeyAuthorizationChallengeResponse( key_authorization=validation).verify( self.achall.chall, KEY.public_key())) self.auth.cleanup([self.achall]) self.assertFalse(os.path.exists(self.validation_path)) self.assertFalse(os.path.exists(self.root_challenge_path)) def test_cleanup_leftovers(self): self.auth.prepare() self.auth.perform([self.achall]) leftover_path = os.path.join(self.root_challenge_path, 'leftover') os.mkdir(leftover_path) self.auth.cleanup([self.achall]) self.assertFalse(os.path.exists(self.validation_path)) self.assertTrue(os.path.exists(self.root_challenge_path)) os.rmdir(leftover_path) @mock.patch('os.rmdir') def test_cleanup_oserror(self, mock_rmdir): self.auth.prepare() self.auth.perform([self.achall]) os_error = OSError() os_error.errno = errno.EACCES mock_rmdir.side_effect = os_error self.assertRaises(OSError, self.auth.cleanup, [self.achall]) self.assertFalse(os.path.exists(self.validation_path)) self.assertTrue(os.path.exists(self.root_challenge_path)) if __name__ == "__main__": unittest.main() # pragma: no cover letsencrypt-0.4.1/letsencrypt/plugins/common_test.py0000644000175000017500000001506212665157707022446 0ustar bmwbmw00000000000000"""Tests for letsencrypt.plugins.common.""" import unittest import mock import OpenSSL from acme import challenges from acme import jose from letsencrypt import achallenges from letsencrypt.tests import acme_util from letsencrypt.tests import test_util class NamespaceFunctionsTest(unittest.TestCase): """Tests for letsencrypt.plugins.common.*_namespace functions.""" def test_option_namespace(self): from letsencrypt.plugins.common import option_namespace self.assertEqual("foo-", option_namespace("foo")) def test_dest_namespace(self): from letsencrypt.plugins.common import dest_namespace self.assertEqual("foo_", dest_namespace("foo")) def test_dest_namespace_with_dashes(self): from letsencrypt.plugins.common import dest_namespace self.assertEqual("foo_bar_", dest_namespace("foo-bar")) class PluginTest(unittest.TestCase): """Test for letsencrypt.plugins.common.Plugin.""" def setUp(self): from letsencrypt.plugins.common import Plugin class MockPlugin(Plugin): # pylint: disable=missing-docstring @classmethod def add_parser_arguments(cls, add): add("foo-bar", dest="different_to_foo_bar", x=1, y=None) self.plugin_cls = MockPlugin self.config = mock.MagicMock() self.plugin = MockPlugin(config=self.config, name="mock") def test_init(self): self.assertEqual("mock", self.plugin.name) self.assertEqual(self.config, self.plugin.config) def test_option_namespace(self): self.assertEqual("mock-", self.plugin.option_namespace) def test_option_name(self): self.assertEqual("mock-foo_bar", self.plugin.option_name("foo_bar")) def test_dest_namespace(self): self.assertEqual("mock_", self.plugin.dest_namespace) def test_dest(self): self.assertEqual("mock_foo_bar", self.plugin.dest("foo-bar")) self.assertEqual("mock_foo_bar", self.plugin.dest("foo_bar")) def test_conf(self): self.assertEqual(self.config.mock_foo_bar, self.plugin.conf("foo-bar")) def test_inject_parser_options(self): parser = mock.MagicMock() self.plugin_cls.inject_parser_options(parser, "mock") # note that inject_parser_options doesn't check if dest has # correct prefix parser.add_argument.assert_called_once_with( "--mock-foo-bar", dest="different_to_foo_bar", x=1, y=None) class AddrTest(unittest.TestCase): """Tests for letsencrypt.client.plugins.common.Addr.""" def setUp(self): from letsencrypt.plugins.common import Addr self.addr1 = Addr.fromstring("192.168.1.1") self.addr2 = Addr.fromstring("192.168.1.1:*") self.addr3 = Addr.fromstring("192.168.1.1:80") def test_fromstring(self): self.assertEqual(self.addr1.get_addr(), "192.168.1.1") self.assertEqual(self.addr1.get_port(), "") self.assertEqual(self.addr2.get_addr(), "192.168.1.1") self.assertEqual(self.addr2.get_port(), "*") self.assertEqual(self.addr3.get_addr(), "192.168.1.1") self.assertEqual(self.addr3.get_port(), "80") def test_str(self): self.assertEqual(str(self.addr1), "192.168.1.1") self.assertEqual(str(self.addr2), "192.168.1.1:*") self.assertEqual(str(self.addr3), "192.168.1.1:80") def test_get_addr_obj(self): self.assertEqual(str(self.addr1.get_addr_obj("443")), "192.168.1.1:443") self.assertEqual(str(self.addr2.get_addr_obj("")), "192.168.1.1") self.assertEqual(str(self.addr1.get_addr_obj("*")), "192.168.1.1:*") def test_eq(self): self.assertEqual(self.addr1, self.addr2.get_addr_obj("")) self.assertNotEqual(self.addr1, self.addr2) self.assertFalse(self.addr1 == 3333) def test_set_inclusion(self): from letsencrypt.plugins.common import Addr set_a = set([self.addr1, self.addr2]) addr1b = Addr.fromstring("192.168.1.1") addr2b = Addr.fromstring("192.168.1.1:*") set_b = set([addr1b, addr2b]) self.assertEqual(set_a, set_b) class TLSSNI01Test(unittest.TestCase): """Tests for letsencrypt.plugins.common.TLSSNI01.""" auth_key = jose.JWKRSA.load(test_util.load_vector("rsa512_key.pem")) achalls = [ achallenges.KeyAuthorizationAnnotatedChallenge( challb=acme_util.chall_to_challb( challenges.TLSSNI01(token=b'token1'), "pending"), domain="encryption-example.demo", account_key=auth_key), achallenges.KeyAuthorizationAnnotatedChallenge( challb=acme_util.chall_to_challb( challenges.TLSSNI01(token=b'token2'), "pending"), domain="letsencrypt.demo", account_key=auth_key), ] def setUp(self): from letsencrypt.plugins.common import TLSSNI01 self.sni = TLSSNI01(configurator=mock.MagicMock()) def test_add_chall(self): self.sni.add_chall(self.achalls[0], 0) self.assertEqual(1, len(self.sni.achalls)) self.assertEqual([0], self.sni.indices) def test_setup_challenge_cert(self): # This is a helper function that can be used for handling # open context managers more elegantly. It avoids dealing with # __enter__ and __exit__ calls. # http://www.voidspace.org.uk/python/mock/helpers.html#mock.mock_open mock_open, mock_safe_open = mock.mock_open(), mock.mock_open() response = challenges.TLSSNI01Response() achall = mock.MagicMock() key = test_util.load_pyopenssl_private_key("rsa512_key.pem") achall.response_and_validation.return_value = ( response, (test_util.load_cert("cert.pem"), key)) with mock.patch("letsencrypt.plugins.common.open", mock_open, create=True): with mock.patch("letsencrypt.plugins.common.le_util.safe_open", mock_safe_open): # pylint: disable=protected-access self.assertEqual(response, self.sni._setup_challenge_cert( achall, "randomS1")) # pylint: disable=no-member mock_open.assert_called_once_with(self.sni.get_cert_path(achall), "wb") mock_open.return_value.write.assert_called_once_with( test_util.load_vector("cert.pem")) mock_safe_open.assert_called_once_with( self.sni.get_key_path(achall), "wb", chmod=0o400) mock_safe_open.return_value.write.assert_called_once_with( OpenSSL.crypto.dump_privatekey(OpenSSL.crypto.FILETYPE_PEM, key)) if __name__ == "__main__": unittest.main() # pragma: no cover letsencrypt-0.4.1/letsencrypt/plugins/__init__.py0000644000175000017500000000004412665157707021650 0ustar bmwbmw00000000000000"""Let's Encrypt client.plugins.""" letsencrypt-0.4.1/letsencrypt/plugins/webroot.py0000644000175000017500000001405312665157707021577 0ustar bmwbmw00000000000000"""Webroot plugin.""" import errno import logging import os from collections import defaultdict import zope.interface import six from acme import challenges from letsencrypt import errors from letsencrypt import interfaces from letsencrypt.plugins import common logger = logging.getLogger(__name__) @zope.interface.implementer(interfaces.IAuthenticator) @zope.interface.provider(interfaces.IPluginFactory) class Authenticator(common.Plugin): """Webroot Authenticator.""" description = "Webroot Authenticator" MORE_INFO = """\ Authenticator plugin that performs http-01 challenge by saving necessary validation resources to appropriate paths on the file system. It expects that there is some other HTTP server configured to serve all files under specified web root ({0}).""" def more_info(self): # pylint: disable=missing-docstring,no-self-use return self.MORE_INFO.format(self.conf("path")) @classmethod def add_parser_arguments(cls, add): # --webroot-path and --webroot-map are added in cli.py because they # are parsed in conjunction with --domains pass def get_chall_pref(self, domain): # pragma: no cover # pylint: disable=missing-docstring,no-self-use,unused-argument return [challenges.HTTP01] def __init__(self, *args, **kwargs): super(Authenticator, self).__init__(*args, **kwargs) self.full_roots = {} self.performed = defaultdict(set) def prepare(self): # pylint: disable=missing-docstring path_map = self.conf("map") if not path_map: raise errors.PluginError( "Missing parts of webroot configuration; please set either " "--webroot-path and --domains, or --webroot-map. Run with " " --help webroot for examples.") for name, path in path_map.items(): if not os.path.isdir(path): raise errors.PluginError(path + " does not exist or is not a directory") self.full_roots[name] = os.path.join(path, challenges.HTTP01.URI_ROOT_PATH) logger.debug("Creating root challenges validation dir at %s", self.full_roots[name]) # Change the permissions to be writable (GH #1389) # Umask is used instead of chmod to ensure the client can also # run as non-root (GH #1795) old_umask = os.umask(0o022) try: # This is coupled with the "umask" call above because # os.makedirs's "mode" parameter may not always work: # https://stackoverflow.com/questions/5231901/permission-problems-when-creating-a-dir-with-os-makedirs-python os.makedirs(self.full_roots[name], 0o0755) # Set owner as parent directory if possible try: stat_path = os.stat(path) os.chown(self.full_roots[name], stat_path.st_uid, stat_path.st_gid) except OSError as exception: if exception.errno == errno.EACCES: logger.debug("Insufficient permissions to change owner and uid - ignoring") else: raise errors.PluginError( "Couldn't create root for {0} http-01 " "challenge responses: {1}", name, exception) except OSError as exception: if exception.errno != errno.EEXIST: raise errors.PluginError( "Couldn't create root for {0} http-01 " "challenge responses: {1}", name, exception) finally: os.umask(old_umask) def perform(self, achalls): # pylint: disable=missing-docstring assert self.full_roots, "Webroot plugin appears to be missing webroot map" return [self._perform_single(achall) for achall in achalls] def _get_root_path(self, achall): try: path = self.full_roots[achall.domain] except KeyError: raise errors.PluginError("Missing --webroot-path for domain: {0}" .format(achall.domain)) if not os.path.exists(path): raise errors.PluginError("Mysteriously missing path {0} for domain: {1}" .format(path, achall.domain)) return path def _get_validation_path(self, root_path, achall): return os.path.join(root_path, achall.chall.encode("token")) def _perform_single(self, achall): response, validation = achall.response_and_validation() root_path = self._get_root_path(achall) validation_path = self._get_validation_path(root_path, achall) logger.debug("Attempting to save validation to %s", validation_path) # Change permissions to be world-readable, owner-writable (GH #1795) old_umask = os.umask(0o022) try: with open(validation_path, "w") as validation_file: validation_file.write(validation.encode()) finally: os.umask(old_umask) self.performed[root_path].add(achall) return response def cleanup(self, achalls): # pylint: disable=missing-docstring for achall in achalls: root_path = self._get_root_path(achall) validation_path = self._get_validation_path(root_path, achall) logger.debug("Removing %s", validation_path) os.remove(validation_path) self.performed[root_path].remove(achall) for root_path, achalls in six.iteritems(self.performed): if not achalls: try: os.rmdir(root_path) logger.debug("All challenges cleaned up, removing %s", root_path) except OSError as exc: if exc.errno == errno.ENOTEMPTY: logger.debug("Challenges cleaned up but %s not empty", root_path) else: raise letsencrypt-0.4.1/letsencrypt/plugins/standalone_test.py0000644000175000017500000002154312665157707023307 0ustar bmwbmw00000000000000"""Tests for letsencrypt.plugins.standalone.""" import argparse import socket import unittest import mock import six from acme import challenges from acme import jose from acme import standalone as acme_standalone from letsencrypt import achallenges from letsencrypt import errors from letsencrypt import interfaces from letsencrypt.tests import acme_util from letsencrypt.tests import test_util class ServerManagerTest(unittest.TestCase): """Tests for letsencrypt.plugins.standalone.ServerManager.""" def setUp(self): from letsencrypt.plugins.standalone import ServerManager self.certs = {} self.http_01_resources = {} self.mgr = ServerManager(self.certs, self.http_01_resources) def test_init(self): self.assertTrue(self.mgr.certs is self.certs) self.assertTrue( self.mgr.http_01_resources is self.http_01_resources) def _test_run_stop(self, challenge_type): server = self.mgr.run(port=0, challenge_type=challenge_type) port = server.socket.getsockname()[1] # pylint: disable=no-member self.assertEqual(self.mgr.running(), {port: server}) self.mgr.stop(port=port) self.assertEqual(self.mgr.running(), {}) def test_run_stop_tls_sni_01(self): self._test_run_stop(challenges.TLSSNI01) def test_run_stop_http_01(self): self._test_run_stop(challenges.HTTP01) def test_run_idempotent(self): server = self.mgr.run(port=0, challenge_type=challenges.HTTP01) port = server.socket.getsockname()[1] # pylint: disable=no-member server2 = self.mgr.run(port=port, challenge_type=challenges.HTTP01) self.assertEqual(self.mgr.running(), {port: server}) self.assertTrue(server is server2) self.mgr.stop(port) self.assertEqual(self.mgr.running(), {}) def test_run_bind_error(self): some_server = socket.socket() some_server.bind(("", 0)) port = some_server.getsockname()[1] self.assertRaises( errors.StandaloneBindError, self.mgr.run, port, challenge_type=challenges.HTTP01) self.assertEqual(self.mgr.running(), {}) class SupportedChallengesValidatorTest(unittest.TestCase): """Tests for plugins.standalone.supported_challenges_validator.""" def _call(self, data): from letsencrypt.plugins.standalone import ( supported_challenges_validator) return supported_challenges_validator(data) def test_correct(self): self.assertEqual("tls-sni-01", self._call("tls-sni-01")) self.assertEqual("http-01", self._call("http-01")) self.assertEqual("tls-sni-01,http-01", self._call("tls-sni-01,http-01")) self.assertEqual("http-01,tls-sni-01", self._call("http-01,tls-sni-01")) def test_unrecognized(self): assert "foo" not in challenges.Challenge.TYPES self.assertRaises(argparse.ArgumentTypeError, self._call, "foo") def test_not_subset(self): self.assertRaises(argparse.ArgumentTypeError, self._call, "dns") class AuthenticatorTest(unittest.TestCase): """Tests for letsencrypt.plugins.standalone.Authenticator.""" def setUp(self): from letsencrypt.plugins.standalone import Authenticator self.config = mock.MagicMock( tls_sni_01_port=1234, http01_port=4321, standalone_supported_challenges="tls-sni-01,http-01") self.auth = Authenticator(self.config, name="standalone") def test_supported_challenges(self): self.assertEqual(self.auth.supported_challenges, [challenges.TLSSNI01, challenges.HTTP01]) def test_supported_challenges_configured(self): self.config.standalone_supported_challenges = "tls-sni-01" self.assertEqual(self.auth.supported_challenges, [challenges.TLSSNI01]) def test_more_info(self): self.assertTrue(isinstance(self.auth.more_info(), six.string_types)) def test_get_chall_pref(self): self.assertEqual(self.auth.get_chall_pref(domain=None), [challenges.TLSSNI01, challenges.HTTP01]) def test_get_chall_pref_configured(self): self.config.standalone_supported_challenges = "tls-sni-01" self.assertEqual(self.auth.get_chall_pref(domain=None), [challenges.TLSSNI01]) @mock.patch("letsencrypt.plugins.standalone.util") def test_perform_already_listening(self, mock_util): for chall, port in ((challenges.TLSSNI01.typ, 1234), (challenges.HTTP01.typ, 4321)): mock_util.already_listening.return_value = True self.config.standalone_supported_challenges = chall self.assertRaises( errors.MisconfigurationError, self.auth.perform, []) mock_util.already_listening.assert_called_once_with(port, False) mock_util.already_listening.reset_mock() @mock.patch("letsencrypt.plugins.standalone.zope.component.getUtility") def test_perform(self, unused_mock_get_utility): achalls = [1, 2, 3] self.auth.perform2 = mock.Mock(return_value=mock.sentinel.responses) self.assertEqual(mock.sentinel.responses, self.auth.perform(achalls)) self.auth.perform2.assert_called_once_with(achalls) @mock.patch("letsencrypt.plugins.standalone.zope.component.getUtility") def _test_perform_bind_errors(self, errno, achalls, mock_get_utility): def _perform2(unused_achalls): raise errors.StandaloneBindError(mock.Mock(errno=errno), 1234) self.auth.perform2 = mock.MagicMock(side_effect=_perform2) self.auth.perform(achalls) mock_get_utility.assert_called_once_with(interfaces.IDisplay) notification = mock_get_utility.return_value.notification self.assertEqual(1, notification.call_count) self.assertTrue("1234" in notification.call_args[0][0]) def test_perform_eacces(self): # pylint: disable=no-value-for-parameter self._test_perform_bind_errors(socket.errno.EACCES, []) def test_perform_eaddrinuse(self): # pylint: disable=no-value-for-parameter self._test_perform_bind_errors(socket.errno.EADDRINUSE, []) def test_perfom_unknown_bind_error(self): self.assertRaises( errors.StandaloneBindError, self._test_perform_bind_errors, socket.errno.ENOTCONN, []) def test_perform2(self): domain = b'localhost' key = jose.JWK.load(test_util.load_vector('rsa512_key.pem')) http_01 = achallenges.KeyAuthorizationAnnotatedChallenge( challb=acme_util.HTTP01_P, domain=domain, account_key=key) tls_sni_01 = achallenges.KeyAuthorizationAnnotatedChallenge( challb=acme_util.TLSSNI01_P, domain=domain, account_key=key) self.auth.servers = mock.MagicMock() def _run(port, tls): # pylint: disable=unused-argument return "server{0}".format(port) self.auth.servers.run.side_effect = _run responses = self.auth.perform2([http_01, tls_sni_01]) self.assertTrue(isinstance(responses, list)) self.assertEqual(2, len(responses)) self.assertTrue(isinstance(responses[0], challenges.HTTP01Response)) self.assertTrue(isinstance(responses[1], challenges.TLSSNI01Response)) self.assertEqual(self.auth.servers.run.mock_calls, [ mock.call(4321, challenges.HTTP01), mock.call(1234, challenges.TLSSNI01), ]) self.assertEqual(self.auth.served, { "server1234": set([tls_sni_01]), "server4321": set([http_01]), }) self.assertEqual(1, len(self.auth.http_01_resources)) self.assertEqual(1, len(self.auth.certs)) self.assertEqual(list(self.auth.http_01_resources), [ acme_standalone.HTTP01RequestHandler.HTTP01Resource( acme_util.HTTP01, responses[0], mock.ANY)]) def test_cleanup(self): self.auth.servers = mock.Mock() self.auth.servers.running.return_value = { 1: "server1", 2: "server2", } self.auth.served["server1"].add("chall1") self.auth.served["server2"].update(["chall2", "chall3"]) self.auth.cleanup(["chall1"]) self.assertEqual(self.auth.served, { "server1": set(), "server2": set(["chall2", "chall3"])}) self.auth.servers.stop.assert_called_once_with(1) self.auth.servers.running.return_value = { 2: "server2", } self.auth.cleanup(["chall2"]) self.assertEqual(self.auth.served, { "server1": set(), "server2": set(["chall3"])}) self.assertEqual(1, self.auth.servers.stop.call_count) self.auth.cleanup(["chall3"]) self.assertEqual(self.auth.served, { "server1": set(), "server2": set([])}) self.auth.servers.stop.assert_called_with(2) if __name__ == "__main__": unittest.main() # pragma: no cover letsencrypt-0.4.1/letsencrypt/plugins/manual_test.py0000644000175000017500000001114712665157707022433 0ustar bmwbmw00000000000000"""Tests for letsencrypt.plugins.manual.""" import signal import unittest import mock from acme import challenges from acme import jose from letsencrypt import achallenges from letsencrypt import errors from letsencrypt.tests import acme_util from letsencrypt.tests import test_util KEY = jose.JWKRSA.load(test_util.load_vector("rsa512_key.pem")) class AuthenticatorTest(unittest.TestCase): """Tests for letsencrypt.plugins.manual.Authenticator.""" def setUp(self): from letsencrypt.plugins.manual import Authenticator self.config = mock.MagicMock( http01_port=8080, manual_test_mode=False, manual_public_ip_logging_ok=False, noninteractive_mode=True) self.auth = Authenticator(config=self.config, name="manual") self.achalls = [achallenges.KeyAuthorizationAnnotatedChallenge( challb=acme_util.HTTP01_P, domain="foo.com", account_key=KEY)] config_test_mode = mock.MagicMock( http01_port=8080, manual_test_mode=True, noninteractive_mode=True) self.auth_test_mode = Authenticator( config=config_test_mode, name="manual") def test_prepare(self): self.assertRaises(errors.PluginError, self.auth.prepare) self.auth_test_mode.prepare() # error not raised def test_more_info(self): self.assertTrue(isinstance(self.auth.more_info(), str)) def test_get_chall_pref(self): self.assertTrue(all(issubclass(pref, challenges.Challenge) for pref in self.auth.get_chall_pref("foo.com"))) def test_perform_empty(self): self.assertEqual([], self.auth.perform([])) @mock.patch("letsencrypt.plugins.manual.zope.component.getUtility") @mock.patch("letsencrypt.plugins.manual.sys.stdout") @mock.patch("acme.challenges.HTTP01Response.simple_verify") @mock.patch("__builtin__.raw_input") def test_perform(self, mock_raw_input, mock_verify, mock_stdout, mock_interaction): mock_verify.return_value = True mock_interaction().yesno.return_value = True resp = self.achalls[0].response(KEY) self.assertEqual([resp], self.auth.perform(self.achalls)) self.assertEqual(1, mock_raw_input.call_count) mock_verify.assert_called_with( self.achalls[0].challb.chall, "foo.com", KEY.public_key(), 8080) message = mock_stdout.write.mock_calls[0][1][0] self.assertTrue(self.achalls[0].chall.encode("token") in message) mock_verify.return_value = False with mock.patch("letsencrypt.plugins.manual.logger") as mock_logger: self.auth.perform(self.achalls) mock_logger.warning.assert_called_once_with(mock.ANY) @mock.patch("letsencrypt.plugins.manual.zope.component.getUtility") @mock.patch("letsencrypt.plugins.manual.Authenticator._notify_and_wait") def test_disagree_with_ip_logging(self, mock_notify, mock_interaction): mock_interaction().yesno.return_value = False mock_notify.side_effect = errors.Error("Exception not raised, \ continued execution even after disagreeing with IP logging") self.assertRaises(errors.PluginError, self.auth.perform, self.achalls) @mock.patch("letsencrypt.plugins.manual.subprocess.Popen", autospec=True) def test_perform_test_command_oserror(self, mock_popen): mock_popen.side_effect = OSError self.assertEqual([False], self.auth_test_mode.perform(self.achalls)) @mock.patch("letsencrypt.plugins.manual.socket.socket") @mock.patch("letsencrypt.plugins.manual.time.sleep", autospec=True) @mock.patch("letsencrypt.plugins.manual.subprocess.Popen", autospec=True) def test_perform_test_command_run_failure( self, mock_popen, unused_mock_sleep, unused_mock_socket): mock_popen.poll.return_value = 10 mock_popen.return_value.pid = 1234 self.assertRaises( errors.Error, self.auth_test_mode.perform, self.achalls) def test_cleanup_test_mode_already_terminated(self): # pylint: disable=protected-access self.auth_test_mode._httpd = httpd = mock.Mock() httpd.poll.return_value = 0 self.auth_test_mode.cleanup(self.achalls) @mock.patch("letsencrypt.plugins.manual.os.killpg", autospec=True) def test_cleanup_test_mode_kills_still_running(self, mock_killpg): # pylint: disable=protected-access self.auth_test_mode._httpd = httpd = mock.Mock(pid=1234) httpd.poll.return_value = None self.auth_test_mode.cleanup(self.achalls) mock_killpg.assert_called_once_with(1234, signal.SIGTERM) if __name__ == "__main__": unittest.main() # pragma: no cover letsencrypt-0.4.1/letsencrypt/plugins/standalone.py0000644000175000017500000002331512665157707022247 0ustar bmwbmw00000000000000"""Standalone Authenticator.""" import argparse import collections import logging import socket import threading import OpenSSL import six import zope.interface from acme import challenges from acme import standalone as acme_standalone from letsencrypt import errors from letsencrypt import interfaces from letsencrypt.plugins import common from letsencrypt.plugins import util logger = logging.getLogger(__name__) class ServerManager(object): """Standalone servers manager. Manager for `ACMEServer` and `ACMETLSServer` instances. `certs` and `http_01_resources` correspond to `acme.crypto_util.SSLSocket.certs` and `acme.crypto_util.SSLSocket.http_01_resources` respectively. All created servers share the same certificates and resources, so if you're running both TLS and non-TLS instances, HTTP01 handlers will serve the same URLs! """ _Instance = collections.namedtuple("_Instance", "server thread") def __init__(self, certs, http_01_resources): self._instances = {} self.certs = certs self.http_01_resources = http_01_resources def run(self, port, challenge_type): """Run ACME server on specified ``port``. This method is idempotent, i.e. all calls with the same pair of ``(port, challenge_type)`` will reuse the same server. :param int port: Port to run the server on. :param challenge_type: Subclass of `acme.challenges.Challenge`, either `acme.challenge.HTTP01` or `acme.challenges.TLSSNI01`. :returns: Server instance. :rtype: ACMEServerMixin """ assert challenge_type in (challenges.TLSSNI01, challenges.HTTP01) if port in self._instances: return self._instances[port].server address = ("", port) try: if challenge_type is challenges.TLSSNI01: server = acme_standalone.TLSSNI01Server(address, self.certs) else: # challenges.HTTP01 server = acme_standalone.HTTP01Server( address, self.http_01_resources) except socket.error as error: raise errors.StandaloneBindError(error, port) thread = threading.Thread( # pylint: disable=no-member target=server.serve_forever) thread.start() # if port == 0, then random free port on OS is taken # pylint: disable=no-member real_port = server.socket.getsockname()[1] self._instances[real_port] = self._Instance(server, thread) return server def stop(self, port): """Stop ACME server running on the specified ``port``. :param int port: """ instance = self._instances[port] logger.debug("Stopping server at %s:%d...", *instance.server.socket.getsockname()[:2]) instance.server.shutdown() # Not calling server_close causes problems when renewing multiple # certs with `letsencrypt renew` using TLSSNI01 and PyOpenSSL 0.13 instance.server.server_close() instance.thread.join() del self._instances[port] def running(self): """Return all running instances. Once the server is stopped using `stop`, it will not be returned. :returns: Mapping from ``port`` to ``server``. :rtype: tuple """ return dict((port, instance.server) for port, instance in six.iteritems(self._instances)) SUPPORTED_CHALLENGES = [challenges.TLSSNI01, challenges.HTTP01] def supported_challenges_validator(data): """Supported challenges validator for the `argparse`. It should be passed as `type` argument to `add_argument`. """ challs = data.split(",") unrecognized = [name for name in challs if name not in challenges.Challenge.TYPES] if unrecognized: raise argparse.ArgumentTypeError( "Unrecognized challenges: {0}".format(", ".join(unrecognized))) choices = set(chall.typ for chall in SUPPORTED_CHALLENGES) if not set(challs).issubset(choices): raise argparse.ArgumentTypeError( "Plugin does not support the following (valid) " "challenges: {0}".format(", ".join(set(challs) - choices))) return data @zope.interface.implementer(interfaces.IAuthenticator) @zope.interface.provider(interfaces.IPluginFactory) class Authenticator(common.Plugin): """Standalone Authenticator. This authenticator creates its own ephemeral TCP listener on the necessary port in order to respond to incoming tls-sni-01 and http-01 challenges from the certificate authority. Therefore, it does not rely on any existing server program. """ description = "Automatically use a temporary webserver" def __init__(self, *args, **kwargs): super(Authenticator, self).__init__(*args, **kwargs) # one self-signed key for all tls-sni-01 certificates self.key = OpenSSL.crypto.PKey() self.key.generate_key(OpenSSL.crypto.TYPE_RSA, 2048) self.served = collections.defaultdict(set) # Stuff below is shared across threads (i.e. servers read # values, main thread writes). Due to the nature of CPython's # GIL, the operations are safe, c.f. # https://docs.python.org/2/faq/library.html#what-kinds-of-global-value-mutation-are-thread-safe self.certs = {} self.http_01_resources = set() self.servers = ServerManager(self.certs, self.http_01_resources) @classmethod def add_parser_arguments(cls, add): add("supported-challenges", help="Supported challenges. Preferred in the order they are listed.", type=supported_challenges_validator, default=",".join(chall.typ for chall in SUPPORTED_CHALLENGES)) @property def supported_challenges(self): """Challenges supported by this plugin.""" return [challenges.Challenge.TYPES[name] for name in self.conf("supported-challenges").split(",")] @property def _necessary_ports(self): necessary_ports = set() if challenges.HTTP01 in self.supported_challenges: necessary_ports.add(self.config.http01_port) if challenges.TLSSNI01 in self.supported_challenges: necessary_ports.add(self.config.tls_sni_01_port) return necessary_ports def more_info(self): # pylint: disable=missing-docstring return("This authenticator creates its own ephemeral TCP listener " "on the necessary port in order to respond to incoming " "tls-sni-01 and http-01 challenges from the certificate " "authority. Therefore, it does not rely on any existing " "server program.") def prepare(self): # pylint: disable=missing-docstring pass def get_chall_pref(self, domain): # pylint: disable=unused-argument,missing-docstring return self.supported_challenges def perform(self, achalls): # pylint: disable=missing-docstring renewer = self.config.verb == "renew" if any(util.already_listening(port, renewer) for port in self._necessary_ports): raise errors.MisconfigurationError( "At least one of the (possibly) required ports is " "already taken.") try: return self.perform2(achalls) except errors.StandaloneBindError as error: display = zope.component.getUtility(interfaces.IDisplay) if error.socket_error.errno == socket.errno.EACCES: display.notification( "Could not bind TCP port {0} because you don't have " "the appropriate permissions (for example, you " "aren't running this program as " "root).".format(error.port)) elif error.socket_error.errno == socket.errno.EADDRINUSE: display.notification( "Could not bind TCP port {0} because it is already in " "use by another process on this system (such as a web " "server). Please stop the program in question and then " "try again.".format(error.port)) else: raise # XXX: How to handle unknown errors in binding? def perform2(self, achalls): """Perform achallenges without IDisplay interaction.""" responses = [] for achall in achalls: if isinstance(achall.chall, challenges.HTTP01): server = self.servers.run( self.config.http01_port, challenges.HTTP01) response, validation = achall.response_and_validation() self.http_01_resources.add( acme_standalone.HTTP01RequestHandler.HTTP01Resource( chall=achall.chall, response=response, validation=validation)) else: # tls-sni-01 server = self.servers.run( self.config.tls_sni_01_port, challenges.TLSSNI01) response, (cert, _) = achall.response_and_validation( cert_key=self.key) self.certs[response.z_domain] = (self.key, cert) self.served[server].add(achall) responses.append(response) return responses def cleanup(self, achalls): # pylint: disable=missing-docstring # reduce self.served and close servers if none challenges are served for server, server_achalls in self.served.items(): for achall in achalls: if achall in server_achalls: server_achalls.remove(achall) for port, server in six.iteritems(self.servers.running()): if not self.served[server]: self.servers.stop(port) letsencrypt-0.4.1/letsencrypt/plugins/common.py0000644000175000017500000001660312665157707021411 0ustar bmwbmw00000000000000"""Plugin common functions.""" import os import re import shutil import tempfile import OpenSSL import pkg_resources import zope.interface from acme.jose import util as jose_util from letsencrypt import constants from letsencrypt import interfaces from letsencrypt import le_util def option_namespace(name): """ArgumentParser options namespace (prefix of all options).""" return name + "-" def dest_namespace(name): """ArgumentParser dest namespace (prefix of all destinations).""" return name.replace("-", "_") + "_" private_ips_regex = re.compile( r"(^127\.0\.0\.1)|(^10\.)|(^172\.1[6-9]\.)|" r"(^172\.2[0-9]\.)|(^172\.3[0-1]\.)|(^192\.168\.)") hostname_regex = re.compile( r"^(([a-z0-9]|[a-z0-9][a-z0-9\-]*[a-z0-9])\.)*[a-z]+$", re.IGNORECASE) @zope.interface.implementer(interfaces.IPlugin) class Plugin(object): """Generic plugin.""" # provider is not inherited, subclasses must define it on their own # @zope.interface.provider(interfaces.IPluginFactory) def __init__(self, config, name): self.config = config self.name = name @jose_util.abstractclassmethod def add_parser_arguments(cls, add): """Add plugin arguments to the CLI argument parser. :param callable add: Function that proxies calls to `argparse.ArgumentParser.add_argument` prepending options with unique plugin name prefix. NOTE: if you add argpase arguments such that users setting them can create a config entry that python's bool() would consider false (ie, the use might set the variable to "", [], 0, etc), please ensure that cli._set_by_cli() works for your variable. """ @classmethod def inject_parser_options(cls, parser, name): """Inject parser options. See `~.IPlugin.inject_parser_options` for docs. """ # dummy function, doesn't check if dest.startswith(self.dest_namespace) def add(arg_name_no_prefix, *args, **kwargs): # pylint: disable=missing-docstring return parser.add_argument( "--{0}{1}".format(option_namespace(name), arg_name_no_prefix), *args, **kwargs) return cls.add_parser_arguments(add) @property def option_namespace(self): """ArgumentParser options namespace (prefix of all options).""" return option_namespace(self.name) def option_name(self, name): """Option name (include plugin namespace).""" return self.option_namespace + name @property def dest_namespace(self): """ArgumentParser dest namespace (prefix of all destinations).""" return dest_namespace(self.name) def dest(self, var): """Find a destination for given variable ``var``.""" # this should do exactly the same what ArgumentParser(arg), # does to "arg" to compute "dest" return self.dest_namespace + var.replace("-", "_") def conf(self, var): """Find a configuration value for variable ``var``.""" return getattr(self.config, self.dest(var)) # other class Addr(object): r"""Represents an virtual host address. :param str addr: addr part of vhost address :param str port: port number or \*, or "" """ def __init__(self, tup): self.tup = tup @classmethod def fromstring(cls, str_addr): """Initialize Addr from string.""" tup = str_addr.partition(':') return cls((tup[0], tup[2])) def __str__(self): if self.tup[1]: return "%s:%s" % self.tup return self.tup[0] def __eq__(self, other): if isinstance(other, self.__class__): return self.tup == other.tup return False def __hash__(self): return hash(self.tup) def get_addr(self): """Return addr part of Addr object.""" return self.tup[0] def get_port(self): """Return port.""" return self.tup[1] def get_addr_obj(self, port): """Return new address object with same addr and new port.""" return self.__class__((self.tup[0], port)) class TLSSNI01(object): """Abstract base for TLS-SNI-01 challenge performers""" def __init__(self, configurator): self.configurator = configurator self.achalls = [] self.indices = [] self.challenge_conf = os.path.join( configurator.config.config_dir, "le_tls_sni_01_cert_challenge.conf") # self.completed = 0 def add_chall(self, achall, idx=None): """Add challenge to TLSSNI01 object to perform at once. :param .KeyAuthorizationAnnotatedChallenge achall: Annotated TLSSNI01 challenge. :param int idx: index to challenge in a larger array """ self.achalls.append(achall) if idx is not None: self.indices.append(idx) def get_cert_path(self, achall): """Returns standardized name for challenge certificate. :param .KeyAuthorizationAnnotatedChallenge achall: Annotated tls-sni-01 challenge. :returns: certificate file name :rtype: str """ return os.path.join(self.configurator.config.work_dir, achall.chall.encode("token") + ".crt") def get_key_path(self, achall): """Get standardized path to challenge key.""" return os.path.join(self.configurator.config.work_dir, achall.chall.encode("token") + '.pem') def _setup_challenge_cert(self, achall, cert_key=None): """Generate and write out challenge certificate.""" cert_path = self.get_cert_path(achall) key_path = self.get_key_path(achall) # Register the path before you write out the file self.configurator.reverter.register_file_creation(True, key_path) self.configurator.reverter.register_file_creation(True, cert_path) response, (cert, key) = achall.response_and_validation( cert_key=cert_key) cert_pem = OpenSSL.crypto.dump_certificate( OpenSSL.crypto.FILETYPE_PEM, cert) key_pem = OpenSSL.crypto.dump_privatekey( OpenSSL.crypto.FILETYPE_PEM, key) # Write out challenge cert and key with open(cert_path, "wb") as cert_chall_fd: cert_chall_fd.write(cert_pem) with le_util.safe_open(key_path, 'wb', chmod=0o400) as key_file: key_file.write(key_pem) return response # test utils used by letsencrypt_apache/letsencrypt_nginx (hence # "pragma: no cover") TODO: this might quickly lead to dead code (also # c.f. #383) def setup_ssl_options(config_dir, src, dest): # pragma: no cover """Move the ssl_options into position and return the path.""" option_path = os.path.join(config_dir, dest) shutil.copyfile(src, option_path) return option_path def dir_setup(test_dir, pkg): # pragma: no cover """Setup the directories necessary for the configurator.""" temp_dir = tempfile.mkdtemp("temp") config_dir = tempfile.mkdtemp("config") work_dir = tempfile.mkdtemp("work") os.chmod(temp_dir, constants.CONFIG_DIRS_MODE) os.chmod(config_dir, constants.CONFIG_DIRS_MODE) os.chmod(work_dir, constants.CONFIG_DIRS_MODE) test_configs = pkg_resources.resource_filename( pkg, os.path.join("testdata", test_dir)) shutil.copytree( test_configs, os.path.join(temp_dir, test_dir), symlinks=True) return temp_dir, config_dir, work_dir letsencrypt-0.4.1/letsencrypt/plugins/null.py0000644000175000017500000000262512665157707021072 0ustar bmwbmw00000000000000"""Null plugin.""" import logging import zope.component import zope.interface from letsencrypt import interfaces from letsencrypt.plugins import common logger = logging.getLogger(__name__) @zope.interface.implementer(interfaces.IInstaller) @zope.interface.provider(interfaces.IPluginFactory) class Installer(common.Plugin): """Null installer.""" description = "Null Installer" hidden = True # pylint: disable=missing-docstring,no-self-use def prepare(self): pass # pragma: no cover def more_info(self): return "Installer that doesn't do anything (for testing)." def get_all_names(self): return [] def deploy_cert(self, domain, cert_path, key_path, chain_path=None, fullchain_path=None): pass # pragma: no cover def enhance(self, domain, enhancement, options=None): pass # pragma: no cover def supported_enhancements(self): return [] def get_all_certs_keys(self): return [] def save(self, title=None, temporary=False): pass # pragma: no cover def rollback_checkpoints(self, rollback=1): pass # pragma: no cover def recovery_routine(self): pass # pragma: no cover def view_config_changes(self): pass # pragma: no cover def config_test(self): pass # pragma: no cover def restart(self): pass # pragma: no cover letsencrypt-0.4.1/letsencrypt/plugins/disco.py0000644000175000017500000002106112665157707021214 0ustar bmwbmw00000000000000"""Utilities for plugins discovery and selection.""" import collections import logging import pkg_resources import zope.interface from letsencrypt import constants from letsencrypt import errors from letsencrypt import interfaces logger = logging.getLogger(__name__) class PluginEntryPoint(object): """Plugin entry point.""" PREFIX_FREE_DISTRIBUTIONS = [ "letsencrypt", "letsencrypt-apache", "letsencrypt-nginx", ] """Distributions for which prefix will be omitted.""" # this object is mutable, don't allow it to be hashed! __hash__ = None def __init__(self, entry_point): self.name = self.entry_point_to_plugin_name(entry_point) self.plugin_cls = entry_point.load() self.entry_point = entry_point self._initialized = None self._prepared = None @classmethod def entry_point_to_plugin_name(cls, entry_point): """Unique plugin name for an ``entry_point``""" if entry_point.dist.key in cls.PREFIX_FREE_DISTRIBUTIONS: return entry_point.name return entry_point.dist.key + ":" + entry_point.name @property def description(self): """Description of the plugin.""" return self.plugin_cls.description @property def description_with_name(self): """Description with name. Handy for UI.""" return "{0} ({1})".format(self.description, self.name) @property def hidden(self): """Should this plugin be hidden from UI?""" return getattr(self.plugin_cls, "hidden", False) def ifaces(self, *ifaces_groups): """Does plugin implements specified interface groups?""" return not ifaces_groups or any( all(iface.implementedBy(self.plugin_cls) for iface in ifaces) for ifaces in ifaces_groups) @property def initialized(self): """Has the plugin been initialized already?""" return self._initialized is not None def init(self, config=None): """Memoized plugin inititialization.""" if not self.initialized: self.entry_point.require() # fetch extras! self._initialized = self.plugin_cls(config, self.name) return self._initialized def verify(self, ifaces): """Verify that the plugin conforms to the specified interfaces.""" assert self.initialized for iface in ifaces: # zope.interface.providedBy(plugin) try: zope.interface.verify.verifyObject(iface, self.init()) except zope.interface.exceptions.BrokenImplementation as error: if iface.implementedBy(self.plugin_cls): logger.debug( "%s implements %s but object does not verify: %s", self.plugin_cls, iface.__name__, error, exc_info=True) return False return True @property def prepared(self): """Has the plugin been prepared already?""" if not self.initialized: logger.debug(".prepared called on uninitialized %r", self) return self._prepared is not None def prepare(self): """Memoized plugin preparation.""" assert self.initialized if self._prepared is None: try: self._initialized.prepare() except errors.MisconfigurationError as error: logger.debug("Misconfigured %r: %s", self, error, exc_info=True) self._prepared = error except errors.NoInstallationError as error: logger.debug( "No installation (%r): %s", self, error, exc_info=True) self._prepared = error except errors.PluginError as error: logger.debug("Other error:(%r): %s", self, error, exc_info=True) self._prepared = error else: self._prepared = True return self._prepared @property def misconfigured(self): """Is plugin misconfigured?""" return isinstance(self._prepared, errors.MisconfigurationError) @property def problem(self): """Return the Exception raised during plugin setup, or None if all is well""" if isinstance(self._prepared, Exception): return self._prepared return None @property def available(self): """Is plugin available, i.e. prepared or misconfigured?""" return self._prepared is True or self.misconfigured def __repr__(self): return "PluginEntryPoint#{0}".format(self.name) def __str__(self): lines = [ "* {0}".format(self.name), "Description: {0}".format(self.plugin_cls.description), "Interfaces: {0}".format(", ".join( iface.__name__ for iface in zope.interface.implementedBy( self.plugin_cls))), "Entry point: {0}".format(self.entry_point), ] if self.initialized: lines.append("Initialized: {0}".format(self.init())) if self.prepared: lines.append("Prep: {0}".format(self.prepare())) return "\n".join(lines) class PluginsRegistry(collections.Mapping): """Plugins registry.""" def __init__(self, plugins): self._plugins = plugins @classmethod def find_all(cls): """Find plugins using setuptools entry points.""" plugins = {} for entry_point in pkg_resources.iter_entry_points( constants.SETUPTOOLS_PLUGINS_ENTRY_POINT): plugin_ep = PluginEntryPoint(entry_point) assert plugin_ep.name not in plugins, ( "PREFIX_FREE_DISTRIBUTIONS messed up") # providedBy | pylint: disable=no-member if interfaces.IPluginFactory.providedBy(plugin_ep.plugin_cls): plugins[plugin_ep.name] = plugin_ep else: # pragma: no cover logger.warning( "%r does not provide IPluginFactory, skipping", plugin_ep) return cls(plugins) def __getitem__(self, name): return self._plugins[name] def __iter__(self): return iter(self._plugins) def __len__(self): return len(self._plugins) def init(self, config): """Initialize all plugins in the registry.""" return [plugin_ep.init(config) for plugin_ep in self._plugins.itervalues()] def filter(self, pred): """Filter plugins based on predicate.""" return type(self)(dict((name, plugin_ep) for name, plugin_ep in self._plugins.iteritems() if pred(plugin_ep))) def visible(self): """Filter plugins based on visibility.""" return self.filter(lambda plugin_ep: not plugin_ep.hidden) def ifaces(self, *ifaces_groups): """Filter plugins based on interfaces.""" # pylint: disable=star-args return self.filter(lambda p_ep: p_ep.ifaces(*ifaces_groups)) def verify(self, ifaces): """Filter plugins based on verification.""" return self.filter(lambda p_ep: p_ep.verify(ifaces)) def prepare(self): """Prepare all plugins in the registry.""" return [plugin_ep.prepare() for plugin_ep in self._plugins.itervalues()] def available(self): """Filter plugins based on availability.""" return self.filter(lambda p_ep: p_ep.available) # succefully prepared + misconfigured def find_init(self, plugin): """Find an initialized plugin. This is particularly useful for finding a name for the plugin (although `.IPluginFactory.__call__` takes ``name`` as one of the arguments, ``IPlugin.name`` is not part of the interface):: # plugin is an instance providing IPlugin, initialized # somewhere else in the code plugin_registry.find_init(plugin).name Returns ``None`` if ``plugin`` is not found in the registry. """ # use list instead of set because PluginEntryPoint is not hashable candidates = [plugin_ep for plugin_ep in self._plugins.itervalues() if plugin_ep.initialized and plugin_ep.init() is plugin] assert len(candidates) <= 1 if candidates: return candidates[0] else: return None def __repr__(self): return "{0}({1})".format( self.__class__.__name__, ','.join( repr(p_ep) for p_ep in self._plugins.itervalues())) def __str__(self): if not self._plugins: return "No plugins" return "\n\n".join(str(p_ep) for p_ep in self._plugins.itervalues()) letsencrypt-0.4.1/letsencrypt/plugins/null_test.py0000644000175000017500000000124612665157707022127 0ustar bmwbmw00000000000000"""Tests for letsencrypt.plugins.null.""" import unittest import mock class InstallerTest(unittest.TestCase): """Tests for letsencrypt.plugins.null.Installer.""" def setUp(self): from letsencrypt.plugins.null import Installer self.installer = Installer(config=mock.MagicMock(), name="null") def test_it(self): self.assertTrue(isinstance(self.installer.more_info(), str)) self.assertEqual([], self.installer.get_all_names()) self.assertEqual([], self.installer.supported_enhancements()) self.assertEqual([], self.installer.get_all_certs_keys()) if __name__ == "__main__": unittest.main() # pragma: no cover letsencrypt-0.4.1/letsencrypt/plugins/util.py0000644000175000017500000000557312665157707021102 0ustar bmwbmw00000000000000"""Plugin utilities.""" import logging import socket import psutil import zope.component from letsencrypt import interfaces logger = logging.getLogger(__name__) def already_listening(port, renewer=False): """Check if a process is already listening on the port. If so, also tell the user via a display notification. .. warning:: On some operating systems, this function can only usefully be run as root. :param int port: The TCP port in question. :returns: True or False. """ try: net_connections = psutil.net_connections() except psutil.AccessDenied as error: logger.info("Access denied when trying to list network " "connections: %s. Are you root?", error) # this function is just a pre-check that often causes false # positives and problems in testing (c.f. #680 on Mac, #255 # generally); we will fail later in bind() anyway return False listeners = [conn.pid for conn in net_connections if conn.status == 'LISTEN' and conn.type == socket.SOCK_STREAM and conn.laddr[1] == port] try: if listeners and listeners[0] is not None: # conn.pid may be None if the current process doesn't have # permission to identify the listening process! Additionally, # listeners may have more than one element if separate # sockets have bound the same port on separate interfaces. # We currently only have UI to notify the user about one # of them at a time. pid = listeners[0] name = psutil.Process(pid).name() display = zope.component.getUtility(interfaces.IDisplay) extra = "" if renewer: extra = ( " For automated renewal, you may want to use a script that stops" " and starts your webserver. You can find an example at" " https://letsencrypt.org/howitworks/#writing-your-own-renewal-script" ". Alternatively you can use the webroot plugin to renew without" " needing to stop and start your webserver.") display.notification( "The program {0} (process ID {1}) is already listening " "on TCP port {2}. This will prevent us from binding to " "that port. Please stop the {0} program temporarily " "and then try again.{3}".format(name, pid, port, extra), height=13) return True except (psutil.NoSuchProcess, psutil.AccessDenied): # Perhaps the result of a race where the process could have # exited or relinquished the port (NoSuchProcess), or the result # of an OS policy where we're not allowed to look up the process # name (AccessDenied). pass return False letsencrypt-0.4.1/letsencrypt/account.py0000644000175000017500000001627512665157707020101 0ustar bmwbmw00000000000000"""Creates ACME accounts for server.""" import datetime import hashlib import logging import os import socket from cryptography.hazmat.primitives import serialization import pyrfc3339 import pytz import zope.component from acme import fields as acme_fields from acme import jose from acme import messages from letsencrypt import errors from letsencrypt import interfaces from letsencrypt import le_util logger = logging.getLogger(__name__) class Account(object): # pylint: disable=too-few-public-methods """ACME protocol registration. :ivar .RegistrationResource regr: Registration Resource :ivar .JWK key: Authorized Account Key :ivar .Meta: Account metadata :ivar str id: Globally unique account identifier. """ class Meta(jose.JSONObjectWithFields): """Account metadata :ivar datetime.datetime creation_dt: Creation date and time (UTC). :ivar str creation_host: FQDN of host, where account has been created. .. note:: ``creation_dt`` and ``creation_host`` are useful in cross-machine migration scenarios. """ creation_dt = acme_fields.RFC3339Field("creation_dt") creation_host = jose.Field("creation_host") def __init__(self, regr, key, meta=None): self.key = key self.regr = regr self.meta = self.Meta( # pyrfc3339 drops microseconds, make sure __eq__ is sane creation_dt=datetime.datetime.now( tz=pytz.UTC).replace(microsecond=0), creation_host=socket.getfqdn()) if meta is None else meta self.id = hashlib.md5( self.key.key.public_key().public_bytes( encoding=serialization.Encoding.PEM, format=serialization.PublicFormat.SubjectPublicKeyInfo) ).hexdigest() # Implementation note: Email? Multiple accounts can have the # same email address. Registration URI? Assigned by the # server, not guaranteed to be stable over time, nor # canonical URI can be generated. ACME protocol doesn't allow # account key (and thus its fingerprint) to be updated... @property def slug(self): """Short account identification string, useful for UI.""" return "{1}@{0} ({2})".format(pyrfc3339.generate( self.meta.creation_dt), self.meta.creation_host, self.id[:4]) def __repr__(self): return "<{0}({1})>".format(self.__class__.__name__, self.id) def __eq__(self, other): return (isinstance(other, self.__class__) and self.key == other.key and self.regr == other.regr and self.meta == other.meta) def report_new_account(acc, config): """Informs the user about their new Let's Encrypt account.""" reporter = zope.component.queryUtility(interfaces.IReporter) if reporter is None: return reporter.add_message( "Your account credentials have been saved in your Let's Encrypt " "configuration directory at {0}. You should make a secure backup " "of this folder now. This configuration directory will also " "contain certificates and private keys obtained by Let's Encrypt " "so making regular backups of this folder is ideal.".format( config.config_dir), reporter.MEDIUM_PRIORITY) if acc.regr.body.emails: recovery_msg = ("If you lose your account credentials, you can " "recover through e-mails sent to {0}.".format( ", ".join(acc.regr.body.emails))) reporter.add_message(recovery_msg, reporter.HIGH_PRIORITY) class AccountMemoryStorage(interfaces.AccountStorage): """In-memory account strage.""" def __init__(self, initial_accounts=None): self.accounts = initial_accounts if initial_accounts is not None else {} def find_all(self): return self.accounts.values() def save(self, account): if account.id in self.accounts: logger.debug("Overwriting account: %s", account.id) self.accounts[account.id] = account def load(self, account_id): try: return self.accounts[account_id] except KeyError: raise errors.AccountNotFound(account_id) class AccountFileStorage(interfaces.AccountStorage): """Accounts file storage. :ivar .IConfig config: Client configuration """ def __init__(self, config): self.config = config le_util.make_or_verify_dir(config.accounts_dir, 0o700, os.geteuid(), self.config.strict_permissions) def _account_dir_path(self, account_id): return os.path.join(self.config.accounts_dir, account_id) @classmethod def _regr_path(cls, account_dir_path): return os.path.join(account_dir_path, "regr.json") @classmethod def _key_path(cls, account_dir_path): return os.path.join(account_dir_path, "private_key.json") @classmethod def _metadata_path(cls, account_dir_path): return os.path.join(account_dir_path, "meta.json") def find_all(self): try: candidates = os.listdir(self.config.accounts_dir) except OSError: return [] accounts = [] for account_id in candidates: try: accounts.append(self.load(account_id)) except errors.AccountStorageError: logger.debug("Account loading problem", exc_info=True) return accounts def load(self, account_id): account_dir_path = self._account_dir_path(account_id) if not os.path.isdir(account_dir_path): raise errors.AccountNotFound( "Account at %s does not exist" % account_dir_path) try: with open(self._regr_path(account_dir_path)) as regr_file: regr = messages.RegistrationResource.json_loads(regr_file.read()) with open(self._key_path(account_dir_path)) as key_file: key = jose.JWK.json_loads(key_file.read()) with open(self._metadata_path(account_dir_path)) as metadata_file: meta = Account.Meta.json_loads(metadata_file.read()) except IOError as error: raise errors.AccountStorageError(error) acc = Account(regr, key, meta) if acc.id != account_id: raise errors.AccountStorageError( "Account ids mismatch (expected: {0}, found: {1}".format( account_id, acc.id)) return acc def save(self, account): account_dir_path = self._account_dir_path(account.id) le_util.make_or_verify_dir(account_dir_path, 0o700, os.geteuid(), self.config.strict_permissions) try: with open(self._regr_path(account_dir_path), "w") as regr_file: regr_file.write(account.regr.json_dumps()) with le_util.safe_open(self._key_path(account_dir_path), "w", chmod=0o400) as key_file: key_file.write(account.key.json_dumps()) with open(self._metadata_path(account_dir_path), "w") as metadata_file: metadata_file.write(account.meta.json_dumps()) except IOError as error: raise errors.AccountStorageError(error) letsencrypt-0.4.1/letsencrypt/display/0000755000175000017500000000000012665157717017526 5ustar bmwbmw00000000000000letsencrypt-0.4.1/letsencrypt/display/ops.py0000644000175000017500000003062012665157707020701 0ustar bmwbmw00000000000000"""Contains UI methods for LE user operations.""" import logging import os import zope.component from letsencrypt import errors from letsencrypt import interfaces from letsencrypt import le_util from letsencrypt.display import util as display_util logger = logging.getLogger(__name__) # Define a helper function to avoid verbose code util = zope.component.getUtility def choose_plugin(prepared, question): """Allow the user to choose their plugin. :param list prepared: List of `~.PluginEntryPoint`. :param str question: Question to be presented to the user. :returns: Plugin entry point chosen by the user. :rtype: `~.PluginEntryPoint` """ opts = [plugin_ep.description_with_name + (" [Misconfigured]" if plugin_ep.misconfigured else "") for plugin_ep in prepared] while True: disp = util(interfaces.IDisplay) code, index = disp.menu(question, opts, help_label="More Info") if code == display_util.OK: plugin_ep = prepared[index] if plugin_ep.misconfigured: util(interfaces.IDisplay).notification( "The selected plugin encountered an error while parsing " "your server configuration and cannot be used. The error " "was:\n\n{0}".format(plugin_ep.prepare()), height=display_util.HEIGHT, pause=False) else: return plugin_ep elif code == display_util.HELP: if prepared[index].misconfigured: msg = "Reported Error: %s" % prepared[index].prepare() else: msg = prepared[index].init().more_info() util(interfaces.IDisplay).notification( msg, height=display_util.HEIGHT) else: return None def pick_plugin(config, default, plugins, question, ifaces): """Pick plugin. :param letsencrypt.interfaces.IConfig: Configuration :param str default: Plugin name supplied by user or ``None``. :param letsencrypt.plugins.disco.PluginsRegistry plugins: All plugins registered as entry points. :param str question: Question to be presented to the user in case multiple candidates are found. :param list ifaces: Interfaces that plugins must provide. :returns: Initialized plugin. :rtype: IPlugin """ if default is not None: # throw more UX-friendly error if default not in plugins filtered = plugins.filter(lambda p_ep: p_ep.name == default) else: if config.noninteractive_mode: # it's really bad to auto-select the single available plugin in # non-interactive mode, because an update could later add a second # available plugin raise errors.MissingCommandlineFlag( "Missing command line flags. For non-interactive execution, " "you will need to specify a plugin on the command line. Run " "with '--help plugins' to see a list of options, and see " "https://eff.org/letsencrypt-plugins for more detail on what " "the plugins do and how to use them.") filtered = plugins.visible().ifaces(ifaces) filtered.init(config) verified = filtered.verify(ifaces) verified.prepare() prepared = verified.available() if len(prepared) > 1: logger.debug("Multiple candidate plugins: %s", prepared) plugin_ep = choose_plugin(prepared.values(), question) if plugin_ep is None: return None else: return plugin_ep.init() elif len(prepared) == 1: plugin_ep = prepared.values()[0] logger.debug("Single candidate plugin: %s", plugin_ep) if plugin_ep.misconfigured: return None return plugin_ep.init() else: logger.debug("No candidate plugin") return None def pick_authenticator( config, default, plugins, question="How would you " "like to authenticate with the Let's Encrypt CA?"): """Pick authentication plugin.""" return pick_plugin( config, default, plugins, question, (interfaces.IAuthenticator,)) def pick_installer(config, default, plugins, question="How would you like to install certificates?"): """Pick installer plugin.""" return pick_plugin( config, default, plugins, question, (interfaces.IInstaller,)) def pick_configurator( config, default, plugins, question="How would you like to authenticate and install " "certificates?"): """Pick configurator plugin.""" return pick_plugin( config, default, plugins, question, (interfaces.IAuthenticator, interfaces.IInstaller)) def get_email(more=False, invalid=False): """Prompt for valid email address. :param bool more: explain why the email is strongly advisable, but how to skip it :param bool invalid: true if the user just typed something, but it wasn't a valid-looking email :returns: Email or ``None`` if cancelled by user. :rtype: str """ msg = "Enter email address (used for urgent notices and lost key recovery)" if invalid: msg = "There seem to be problems with that address. " + msg if more: msg += ('\n\nIf you really want to skip this, you can run the client with ' '--register-unsafely-without-email but make sure you backup your ' 'account key from /etc/letsencrypt/accounts\n\n') try: code, email = zope.component.getUtility(interfaces.IDisplay).input(msg) except errors.MissingCommandlineFlag: msg = ("You should register before running non-interactively, or provide --agree-tos" " and --email flags") raise errors.MissingCommandlineFlag(msg) if code == display_util.OK: if le_util.safe_email(email): return email else: # TODO catch the server's ACME invalid email address error, and # make a similar call when that happens return get_email(more=True, invalid=(email != "")) else: return None def choose_account(accounts): """Choose an account. :param list accounts: Containing at least one :class:`~letsencrypt.account.Account` """ # Note this will get more complicated once we start recording authorizations labels = [acc.slug for acc in accounts] code, index = util(interfaces.IDisplay).menu( "Please choose an account", labels) if code == display_util.OK: return accounts[index] else: return None def choose_names(installer): """Display screen to select domains to validate. :param installer: An installer object :type installer: :class:`letsencrypt.interfaces.IInstaller` :returns: List of selected names :rtype: `list` of `str` """ if installer is None: logger.debug("No installer, picking names manually") return _choose_names_manually() domains = list(installer.get_all_names()) names = get_valid_domains(domains) if not names: manual = util(interfaces.IDisplay).yesno( "No names were found in your configuration files.{0}You should " "specify ServerNames in your config files in order to allow for " "accurate installation of your certificate.{0}" "If you do use the default vhost, you may specify the name " "manually. Would you like to continue?{0}".format(os.linesep), default=True) if manual: return _choose_names_manually() else: return [] code, names = _filter_names(names) if code == display_util.OK and names: return names else: return [] def get_valid_domains(domains): """Helper method for choose_names that implements basic checks on domain names :param list domains: Domain names to validate :return: List of valid domains :rtype: list """ valid_domains = [] for domain in domains: try: valid_domains.append(le_util.enforce_domain_sanity(domain)) except errors.ConfigurationError: continue return valid_domains def _filter_names(names): """Determine which names the user would like to select from a list. :param list names: domain names :returns: tuple of the form (`code`, `names`) where `code` - str display exit code `names` - list of names selected :rtype: tuple """ code, names = util(interfaces.IDisplay).checklist( "Which names would you like to activate HTTPS for?", tags=names, cli_flag="--domains") return code, [str(s) for s in names] def _choose_names_manually(): """Manually input names for those without an installer.""" code, input_ = util(interfaces.IDisplay).input( "Please enter in your domain name(s) (comma and/or space separated) ", cli_flag="--domains") if code == display_util.OK: invalid_domains = dict() retry_message = "" try: domain_list = display_util.separate_list_input(input_) except UnicodeEncodeError: domain_list = [] retry_message = ( "Internationalized domain names are not presently " "supported.{0}{0}Would you like to re-enter the " "names?{0}").format(os.linesep) for i, domain in enumerate(domain_list): try: domain_list[i] = le_util.enforce_domain_sanity(domain) except errors.ConfigurationError as e: invalid_domains[domain] = e.message if len(invalid_domains): retry_message = ( "One or more of the entered domain names was not valid:" "{0}{0}").format(os.linesep) for domain in invalid_domains: retry_message = retry_message + "{1}: {2}{0}".format( os.linesep, domain, invalid_domains[domain]) retry_message = retry_message + ( "{0}Would you like to re-enter the names?{0}").format( os.linesep) if retry_message: # We had error in input retry = util(interfaces.IDisplay).yesno(retry_message) if retry: return _choose_names_manually() else: return domain_list return [] def success_installation(domains): """Display a box confirming the installation of HTTPS. .. todo:: This should be centered on the screen :param list domains: domain names which were enabled """ util(interfaces.IDisplay).notification( "Congratulations! You have successfully enabled {0}{1}{1}" "You should test your configuration at:{1}{2}".format( _gen_https_names(domains), os.linesep, os.linesep.join(_gen_ssl_lab_urls(domains))), height=(10 + len(domains)), pause=False) def success_renewal(domains, action): """Display a box confirming the renewal of an existing certificate. .. todo:: This should be centered on the screen :param list domains: domain names which were renewed :param str action: can be "reinstall" or "renew" """ util(interfaces.IDisplay).notification( "Your existing certificate has been successfully {3}ed, and the " "new certificate has been installed.{1}{1}" "The new certificate covers the following domains: {0}{1}{1}" "You should test your configuration at:{1}{2}".format( _gen_https_names(domains), os.linesep, os.linesep.join(_gen_ssl_lab_urls(domains)), action), height=(14 + len(domains)), pause=False) def _gen_ssl_lab_urls(domains): """Returns a list of urls. :param list domains: Each domain is a 'str' """ return ["https://www.ssllabs.com/ssltest/analyze.html?d=%s" % dom for dom in domains] def _gen_https_names(domains): """Returns a string of the https domains. Domains are formatted nicely with https:// prepended to each. :param list domains: Each domain is a 'str' """ if len(domains) == 1: return "https://{0}".format(domains[0]) elif len(domains) == 2: return "https://{dom[0]} and https://{dom[1]}".format(dom=domains) elif len(domains) > 2: return "{0}{1}{2}".format( ", ".join("https://%s" % dom for dom in domains[:-1]), ", and https://", domains[-1]) return "" letsencrypt-0.4.1/letsencrypt/display/__init__.py0000644000175000017500000000004712665157707021637 0ustar bmwbmw00000000000000"""Let's Encrypt display utilities.""" letsencrypt-0.4.1/letsencrypt/display/enhancements.py0000644000175000017500000000306612665157707022554 0ustar bmwbmw00000000000000"""Let's Encrypt Enhancement Display""" import logging import zope.component from letsencrypt import errors from letsencrypt import interfaces from letsencrypt.display import util as display_util logger = logging.getLogger(__name__) # Define a helper function to avoid verbose code util = zope.component.getUtility def ask(enhancement): """Display the enhancement to the user. :param str enhancement: One of the :class:`letsencrypt.CONFIG.ENHANCEMENTS` enhancements :returns: True if feature is desired, False otherwise :rtype: bool :raises .errors.Error: if the enhancement provided is not supported """ try: # Call the appropriate function based on the enhancement return DISPATCH[enhancement]() except KeyError: logger.error("Unsupported enhancement given to ask(): %s", enhancement) raise errors.Error("Unsupported Enhancement") def redirect_by_default(): """Determines whether the user would like to redirect to HTTPS. :returns: True if redirect is desired, False otherwise :rtype: bool """ choices = [ ("Easy", "Allow both HTTP and HTTPS access to these sites"), ("Secure", "Make all requests redirect to secure HTTPS access"), ] code, selection = util(interfaces.IDisplay).menu( "Please choose whether HTTPS access is required or optional.", choices, default=0, cli_flag="--redirect / --no-redirect") if code != display_util.OK: return False return selection == 1 DISPATCH = { "redirect": redirect_by_default } letsencrypt-0.4.1/letsencrypt/display/util.py0000644000175000017500000004425312665157707021064 0ustar bmwbmw00000000000000"""Let's Encrypt display.""" import os import textwrap import dialog import zope.interface from letsencrypt import interfaces from letsencrypt import errors WIDTH = 72 HEIGHT = 20 # Display exit codes OK = "ok" """Display exit code indicating user acceptance.""" CANCEL = "cancel" """Display exit code for a user canceling the display.""" HELP = "help" """Display exit code when for when the user requests more help.""" def _wrap_lines(msg): """Format lines nicely to 80 chars. :param str msg: Original message :returns: Formatted message respecting newlines in message :rtype: str """ lines = msg.splitlines() fixed_l = [] for line in lines: fixed_l.append(textwrap.fill(line, 80)) return os.linesep.join(fixed_l) @zope.interface.implementer(interfaces.IDisplay) class NcursesDisplay(object): """Ncurses-based display.""" def __init__(self, width=WIDTH, height=HEIGHT): super(NcursesDisplay, self).__init__() self.dialog = dialog.Dialog() self.width = width self.height = height def notification(self, message, height=10, pause=False): # pylint: disable=unused-argument """Display a notification to the user and wait for user acceptance. .. todo:: It probably makes sense to use one of the transient message types for pause. It isn't straightforward how best to approach the matter though given the context of our messages. http://pythondialog.sourceforge.net/doc/widgets.html#displaying-transient-messages :param str message: Message to display :param int height: Height of the dialog box :param bool pause: Not applicable to NcursesDisplay """ self.dialog.msgbox(message, height, width=self.width) def menu(self, message, choices, ok_label="OK", cancel_label="Cancel", help_label="", **unused_kwargs): """Display a menu. :param str message: title of menu :param choices: menu lines, len must be > 0 :type choices: list of tuples (`tag`, `item`) tags must be unique or list of items (tags will be enumerated) :param str ok_label: label of the OK button :param str help_label: label of the help button :param dict unused_kwargs: absorbs default / cli_args :returns: tuple of the form (`code`, `index`) where `code` - int display exit code `int` - index of the selected item :rtype: tuple """ menu_options = { "choices": choices, "ok_label": ok_label, "cancel_label": cancel_label, "help_button": bool(help_label), "help_label": help_label, "width": self.width, "height": self.height, "menu_height": self.height - 6, } # Can accept either tuples or just the actual choices if choices and isinstance(choices[0], tuple): # pylint: disable=star-args code, selection = self.dialog.menu(message, **menu_options) # Return the selection index for i, choice in enumerate(choices): if choice[0] == selection: return code, i return code, -1 else: # "choices" is not formatted the way the dialog.menu expects... menu_options["choices"] = [ (str(i), choice) for i, choice in enumerate(choices, 1) ] # pylint: disable=star-args code, index = self.dialog.menu(message, **menu_options) if code == CANCEL: return code, -1 return code, int(index) - 1 def input(self, message, **unused_kwargs): """Display an input box to the user. :param str message: Message to display that asks for input. :param dict _kwargs: absorbs default / cli_args :returns: tuple of the form (`code`, `string`) where `code` - int display exit code `string` - input entered by the user """ sections = message.split("\n") # each section takes at least one line, plus extras if it's longer than self.width wordlines = [1 + (len(section)/self.width) for section in sections] height = 6 + sum(wordlines) + len(sections) return self.dialog.inputbox(message, width=self.width, height=height) def yesno(self, message, yes_label="Yes", no_label="No", **unused_kwargs): """Display a Yes/No dialog box. Yes and No label must begin with different letters. :param str message: message to display to user :param str yes_label: label on the "yes" button :param str no_label: label on the "no" button :param dict _kwargs: absorbs default / cli_args :returns: if yes_label was selected :rtype: bool """ return self.dialog.DIALOG_OK == self.dialog.yesno( message, self.height, self.width, yes_label=yes_label, no_label=no_label) def checklist(self, message, tags, default_status=True, **unused_kwargs): """Displays a checklist. :param message: Message to display before choices :param list tags: where each is of type :class:`str` len(tags) > 0 :param bool default_status: If True, items are in a selected state by default. :param dict _kwargs: absorbs default / cli_args :returns: tuple of the form (`code`, `list_tags`) where `code` - int display exit code `list_tags` - list of str tags selected by the user """ choices = [(tag, "", default_status) for tag in tags] return self.dialog.checklist( message, width=self.width, height=self.height, choices=choices) @zope.interface.implementer(interfaces.IDisplay) class FileDisplay(object): """File-based display.""" def __init__(self, outfile): super(FileDisplay, self).__init__() self.outfile = outfile def notification(self, message, height=10, pause=True): # pylint: disable=unused-argument """Displays a notification and waits for user acceptance. :param str message: Message to display :param int height: No effect for FileDisplay :param bool pause: Whether or not the program should pause for the user's confirmation """ side_frame = "-" * 79 message = _wrap_lines(message) self.outfile.write( "{line}{frame}{line}{msg}{line}{frame}{line}".format( line=os.linesep, frame=side_frame, msg=message)) if pause: raw_input("Press Enter to Continue") def menu(self, message, choices, ok_label="", cancel_label="", help_label="", **unused_kwargs): # pylint: disable=unused-argument """Display a menu. .. todo:: This doesn't enable the help label/button (I wasn't sold on any interface I came up with for this). It would be a nice feature :param str message: title of menu :param choices: Menu lines, len must be > 0 :type choices: list of tuples (tag, item) or list of descriptions (tags will be enumerated) :param dict _kwargs: absorbs default / cli_args :returns: tuple of (`code`, `index`) where `code` - str display exit code `index` - int index of the user's selection :rtype: tuple """ self._print_menu(message, choices) code, selection = self._get_valid_int_ans(len(choices)) return code, selection - 1 def input(self, message, **unused_kwargs): # pylint: disable=no-self-use """Accept input from the user. :param str message: message to display to the user :param dict _kwargs: absorbs default / cli_args :returns: tuple of (`code`, `input`) where `code` - str display exit code `input` - str of the user's input :rtype: tuple """ ans = raw_input( textwrap.fill("%s (Enter 'c' to cancel): " % message, 80)) if ans == "c" or ans == "C": return CANCEL, "-1" else: return OK, ans def yesno(self, message, yes_label="Yes", no_label="No", **unused_kwargs): """Query the user with a yes/no question. Yes and No label must begin with different letters, and must contain at least one letter each. :param str message: question for the user :param str yes_label: Label of the "Yes" parameter :param str no_label: Label of the "No" parameter :param dict _kwargs: absorbs default / cli_args :returns: True for "Yes", False for "No" :rtype: bool """ side_frame = ("-" * 79) + os.linesep message = _wrap_lines(message) self.outfile.write("{0}{frame}{msg}{0}{frame}".format( os.linesep, frame=side_frame, msg=message)) while True: ans = raw_input("{yes}/{no}: ".format( yes=_parens_around_char(yes_label), no=_parens_around_char(no_label))) # Couldn't get pylint indentation right with elif # elif doesn't matter in this situation if (ans.startswith(yes_label[0].lower()) or ans.startswith(yes_label[0].upper())): return True if (ans.startswith(no_label[0].lower()) or ans.startswith(no_label[0].upper())): return False def checklist(self, message, tags, default_status=True, **unused_kwargs): # pylint: disable=unused-argument """Display a checklist. :param str message: Message to display to user :param list tags: `str` tags to select, len(tags) > 0 :param bool default_status: Not used for FileDisplay :param dict _kwargs: absorbs default / cli_args :returns: tuple of (`code`, `tags`) where `code` - str display exit code `tags` - list of selected tags :rtype: tuple """ while True: self._print_menu(message, tags) code, ans = self.input("Select the appropriate numbers separated " "by commas and/or spaces") if code == OK: indices = separate_list_input(ans) selected_tags = self._scrub_checklist_input(indices, tags) if selected_tags: return code, selected_tags else: self.outfile.write( "** Error - Invalid selection **%s" % os.linesep) else: return code, [] def _scrub_checklist_input(self, indices, tags): # pylint: disable=no-self-use """Validate input and transform indices to appropriate tags. :param list indices: input :param list tags: Original tags of the checklist :returns: valid tags the user selected :rtype: :class:`list` of :class:`str` """ # They should all be of type int try: indices = [int(index) for index in indices] except ValueError: return [] # Remove duplicates indices = list(set(indices)) # Check all input is within range for index in indices: if index < 1 or index > len(tags): return [] # Transform indices to appropriate tags return [tags[index - 1] for index in indices] def _print_menu(self, message, choices): """Print a menu on the screen. :param str message: title of menu :param choices: Menu lines :type choices: list of tuples (tag, item) or list of descriptions (tags will be enumerated) """ # Can take either tuples or single items in choices list if choices and isinstance(choices[0], tuple): choices = ["%s - %s" % (c[0], c[1]) for c in choices] # Write out the message to the user self.outfile.write( "{new}{msg}{new}".format(new=os.linesep, msg=message)) side_frame = ("-" * 79) + os.linesep self.outfile.write(side_frame) # Write out the menu choices for i, desc in enumerate(choices, 1): self.outfile.write( textwrap.fill("{num}: {desc}".format(num=i, desc=desc), 80)) # Keep this outside of the textwrap self.outfile.write(os.linesep) self.outfile.write(side_frame) def _get_valid_int_ans(self, max_): """Get a numerical selection. :param int max: The maximum entry (len of choices), must be positive :returns: tuple of the form (`code`, `selection`) where `code` - str display exit code ('ok' or cancel') `selection` - int user's selection :rtype: tuple """ selection = -1 if max_ > 1: input_msg = ("Select the appropriate number " "[1-{max_}] then [enter] (press 'c' to " "cancel): ".format(max_=max_)) else: input_msg = ("Press 1 [enter] to confirm the selection " "(press 'c' to cancel): ") while selection < 1: ans = raw_input(input_msg) if ans.startswith("c") or ans.startswith("C"): return CANCEL, -1 try: selection = int(ans) if selection < 1 or selection > max_: selection = -1 raise ValueError except ValueError: self.outfile.write( "{0}** Invalid input **{0}".format(os.linesep)) return OK, selection @zope.interface.implementer(interfaces.IDisplay) class NoninteractiveDisplay(object): """An iDisplay implementation that never asks for interactive user input""" def __init__(self, outfile): super(NoninteractiveDisplay, self).__init__() self.outfile = outfile def _interaction_fail(self, message, cli_flag, extra=""): "Error out in case of an attempt to interact in noninteractive mode" msg = "Missing command line flag or config entry for this setting:\n" msg += message if extra: msg += "\n" + extra if cli_flag: msg += "\n\n(You can set this with the {0} flag)".format(cli_flag) raise errors.MissingCommandlineFlag(msg) def notification(self, message, height=10, pause=False): # pylint: disable=unused-argument """Displays a notification without waiting for user acceptance. :param str message: Message to display to stdout :param int height: No effect for NoninteractiveDisplay :param bool pause: The NoninteractiveDisplay waits for no keyboard """ side_frame = "-" * 79 message = _wrap_lines(message) self.outfile.write( "{line}{frame}{line}{msg}{line}{frame}{line}".format( line=os.linesep, frame=side_frame, msg=message)) def menu(self, message, choices, ok_label=None, cancel_label=None, help_label=None, default=None, cli_flag=None): # pylint: disable=unused-argument,too-many-arguments """Avoid displaying a menu. :param str message: title of menu :param choices: Menu lines, len must be > 0 :type choices: list of tuples (tag, item) or list of descriptions (tags will be enumerated) :param int default: the default choice :param dict kwargs: absorbs various irrelevant labelling arguments :returns: tuple of (`code`, `index`) where `code` - str display exit code `index` - int index of the user's selection :rtype: tuple :raises errors.MissingCommandlineFlag: if there was no default """ if default is None: self._interaction_fail(message, cli_flag, "Choices: " + repr(choices)) return OK, default def input(self, message, default=None, cli_flag=None): """Accept input from the user. :param str message: message to display to the user :returns: tuple of (`code`, `input`) where `code` - str display exit code `input` - str of the user's input :rtype: tuple :raises errors.MissingCommandlineFlag: if there was no default """ if default is None: self._interaction_fail(message, cli_flag) else: return OK, default def yesno(self, message, yes_label=None, no_label=None, default=None, cli_flag=None): # pylint: disable=unused-argument """Decide Yes or No, without asking anybody :param str message: question for the user :param dict kwargs: absorbs yes_label, no_label :raises errors.MissingCommandlineFlag: if there was no default :returns: True for "Yes", False for "No" :rtype: bool """ if default is None: self._interaction_fail(message, cli_flag) else: return default def checklist(self, message, tags, default=None, cli_flag=None, **kwargs): # pylint: disable=unused-argument """Display a checklist. :param str message: Message to display to user :param list tags: `str` tags to select, len(tags) > 0 :param dict kwargs: absorbs default_status arg :returns: tuple of (`code`, `tags`) where `code` - str display exit code `tags` - list of selected tags :rtype: tuple """ if default is None: self._interaction_fail(message, cli_flag, "? ".join(tags)) else: return OK, default def separate_list_input(input_): """Separate a comma or space separated list. :param str input_: input from the user :returns: strings :rtype: list """ no_commas = input_.replace(",", " ") # Each string is naturally unicode, this causes problems with M2Crypto SANs # TODO: check if above is still true when M2Crypto is gone ^ return [str(string) for string in no_commas.split()] def _parens_around_char(label): """Place parens around first character of label. :param str label: Must contain at least one character """ return "({first}){rest}".format(first=label[0], rest=label[1:]) letsencrypt-0.4.1/letsencrypt/le_util.py0000644000175000017500000002460412665157707020075 0ustar bmwbmw00000000000000"""Utilities for all Let's Encrypt.""" import argparse import collections import errno import logging import os import platform import re import socket import stat import subprocess import sys import configargparse from letsencrypt import errors logger = logging.getLogger(__name__) Key = collections.namedtuple("Key", "file pem") # Note: form is the type of data, "pem" or "der" CSR = collections.namedtuple("CSR", "file data form") # ANSI SGR escape codes # Formats text as bold or with increased intensity ANSI_SGR_BOLD = '\033[1m' # Colors text red ANSI_SGR_RED = "\033[31m" # Resets output format ANSI_SGR_RESET = "\033[0m" def run_script(params): """Run the script with the given params. :param list params: List of parameters to pass to Popen """ try: proc = subprocess.Popen(params, stdout=subprocess.PIPE, stderr=subprocess.PIPE) except (OSError, ValueError): msg = "Unable to run the command: %s" % " ".join(params) logger.error(msg) raise errors.SubprocessError(msg) stdout, stderr = proc.communicate() if proc.returncode != 0: msg = "Error while running %s.\n%s\n%s" % ( " ".join(params), stdout, stderr) # Enter recovery routine... logger.error(msg) raise errors.SubprocessError(msg) return stdout, stderr def exe_exists(exe): """Determine whether path/name refers to an executable. :param str exe: Executable path or name :returns: If exe is a valid executable :rtype: bool """ def is_exe(path): """Determine if path is an exe.""" return os.path.isfile(path) and os.access(path, os.X_OK) path, _ = os.path.split(exe) if path: return is_exe(exe) else: for path in os.environ["PATH"].split(os.pathsep): if is_exe(os.path.join(path, exe)): return True return False def make_or_verify_dir(directory, mode=0o755, uid=0, strict=False): """Make sure directory exists with proper permissions. :param str directory: Path to a directory. :param int mode: Directory mode. :param int uid: Directory owner. :raises .errors.Error: if a directory already exists, but has wrong permissions or owner :raises OSError: if invalid or inaccessible file names and paths, or other arguments that have the correct type, but are not accepted by the operating system. """ try: os.makedirs(directory, mode) except OSError as exception: if exception.errno == errno.EEXIST: if strict and not check_permissions(directory, mode, uid): raise errors.Error( "%s exists, but it should be owned by user %d with" "permissions %s" % (directory, uid, oct(mode))) else: raise def check_permissions(filepath, mode, uid=0): """Check file or directory permissions. :param str filepath: Path to the tested file (or directory). :param int mode: Expected file mode. :param int uid: Expected file owner. :returns: True if `mode` and `uid` match, False otherwise. :rtype: bool """ file_stat = os.stat(filepath) return stat.S_IMODE(file_stat.st_mode) == mode and file_stat.st_uid == uid def safe_open(path, mode="w", chmod=None, buffering=None): """Safely open a file. :param str path: Path to a file. :param str mode: Same os `mode` for `open`. :param int chmod: Same as `mode` for `os.open`, uses Python defaults if ``None``. :param int buffering: Same as `bufsize` for `os.fdopen`, uses Python defaults if ``None``. """ # pylint: disable=star-args open_args = () if chmod is None else (chmod,) fdopen_args = () if buffering is None else (buffering,) return os.fdopen( os.open(path, os.O_CREAT | os.O_EXCL | os.O_RDWR, *open_args), mode, *fdopen_args) def _unique_file(path, filename_pat, count, mode): while True: current_path = os.path.join(path, filename_pat(count)) try: return safe_open(current_path, chmod=mode), current_path except OSError as err: # "File exists," is okay, try a different name. if err.errno != errno.EEXIST: raise count += 1 def unique_file(path, mode=0o777): """Safely finds a unique file. :param str path: path/filename.ext :param int mode: File mode :returns: tuple of file object and file name """ path, tail = os.path.split(path) return _unique_file( path, filename_pat=(lambda count: "%04d_%s" % (count, tail)), count=0, mode=mode) def unique_lineage_name(path, filename, mode=0o777): """Safely finds a unique file using lineage convention. :param str path: directory path :param str filename: proposed filename :param int mode: file mode :returns: tuple of file object and file name (which may be modified from the requested one by appending digits to ensure uniqueness) :raises OSError: if writing files fails for an unanticipated reason, such as a full disk or a lack of permission to write to specified location. """ preferred_path = os.path.join(path, "%s.conf" % (filename)) try: return safe_open(preferred_path, chmod=mode), preferred_path except OSError as err: if err.errno != errno.EEXIST: raise return _unique_file( path, filename_pat=(lambda count: "%s-%04d.conf" % (filename, count)), count=1, mode=mode) def safely_remove(path): """Remove a file that may not exist.""" try: os.remove(path) except OSError as err: if err.errno != errno.ENOENT: raise def get_os_info(): """ Get Operating System type/distribution and major version :returns: (os_name, os_version) :rtype: `tuple` of `str` """ info = platform.system_alias( platform.system(), platform.release(), platform.version() ) os_type, os_ver, _ = info os_type = os_type.lower() if os_type.startswith('linux'): info = platform.linux_distribution() # On arch, platform.linux_distribution() is reportedly ('','',''), # so handle it defensively if info[0]: os_type = info[0] if info[1]: os_ver = info[1] elif os_type.startswith('darwin'): os_ver = subprocess.Popen( ["sw_vers", "-productVersion"], stdout=subprocess.PIPE ).communicate()[0] os_ver = os_ver.partition(".")[0] elif os_type.startswith('freebsd'): # eg "9.3-RC3-p1" os_ver = os_ver.partition("-")[0] os_ver = os_ver.partition(".")[0] elif platform.win32_ver()[1]: os_ver = platform.win32_ver()[1] else: # Cases known to fall here: Cygwin python os_ver = '' return os_type, os_ver # Just make sure we don't get pwned... Make sure that it also doesn't # start with a period or have two consecutive periods <- this needs to # be done in addition to the regex EMAIL_REGEX = re.compile("[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+$") def safe_email(email): """Scrub email address before using it.""" if EMAIL_REGEX.match(email) is not None: return not email.startswith(".") and ".." not in email else: logger.warn("Invalid email address: %s.", email) return False def add_deprecated_argument(add_argument, argument_name, nargs): """Adds a deprecated argument with the name argument_name. Deprecated arguments are not shown in the help. If they are used on the command line, a warning is shown stating that the argument is deprecated and no other action is taken. :param callable add_argument: Function that adds arguments to an argument parser/group. :param str argument_name: Name of deprecated argument. :param nargs: Value for nargs when adding the argument to argparse. """ class ShowWarning(argparse.Action): """Action to log a warning when an argument is used.""" def __call__(self, unused1, unused2, unused3, option_string=None): sys.stderr.write( "Use of {0} is deprecated.\n".format(option_string)) configargparse.ACTION_TYPES_THAT_DONT_NEED_A_VALUE.add(ShowWarning) add_argument(argument_name, action=ShowWarning, help=argparse.SUPPRESS, nargs=nargs) def enforce_domain_sanity(domain): """Method which validates domain value and errors out if the requirements are not met. :param domain: Domain to check :type domains: `str` or `unicode` :raises ConfigurationError: for invalid domains and cases where Let's Encrypt currently will not issue certificates :returns: The domain cast to `str`, with ASCII-only contents :rtype: str """ # Check if there's a wildcard domain if domain.startswith("*."): raise errors.ConfigurationError( "Wildcard domains are not supported: {0}".format(domain)) # Punycode if "xn--" in domain: raise errors.ConfigurationError( "Punycode domains are not presently supported: {0}".format(domain)) # Unicode try: domain = domain.encode('ascii').lower() except UnicodeDecodeError: raise errors.ConfigurationError( "Internationalized domain names are not presently supported: {0}" .format(domain)) # Remove trailing dot domain = domain[:-1] if domain.endswith('.') else domain # Explain separately that IP addresses aren't allowed (apart from not # being FQDNs) because hope springs eternal concerning this point try: socket.inet_aton(domain) raise errors.ConfigurationError( "Requested name {0} is an IP address. The Let's Encrypt " "certificate authority will not issue certificates for a " "bare IP address.".format(domain)) except socket.error: # It wasn't an IP address, so that's good pass # FQDN checks from # http://www.mkyong.com/regular-expressions/domain-name-regular-expression-example/ # Characters used, domain parts < 63 chars, tld > 1 < 64 chars # first and last char is not "-" fqdn = re.compile("^((?!-)[A-Za-z0-9-]{1,63}(? https://letsencrypt.readthedocs.org/en/latest/contributing.html letsencrypt-0.4.1/linter_plugin.py0000644000175000017500000000146312665157707016735 0ustar bmwbmw00000000000000"""Let's Encrypt ACME PyLint plugin. http://docs.pylint.org/plugins.html """ from astroid import MANAGER from astroid import nodes def register(unused_linter): """Register this module as PyLint plugin.""" def _transform(cls): # fix the "no-member" error on instances of # letsencrypt.acme.util.ImmutableMap subclasses (instance # attributes are initialized dynamically based on __slots__) # TODO: this is too broad and applies to any tested class... if cls.slots() is not None: for slot in cls.slots(): cls.locals[slot.value] = [nodes.EmptyNode()] if cls.name == 'JSONObjectWithFields': # _fields is magically introduced by JSONObjectWithFieldsMeta cls.locals['_fields'] = [nodes.EmptyNode()] MANAGER.register_transform(nodes.Class, _transform) letsencrypt-0.4.1/LICENSE.txt0000644000175000017500000002631012665157707015331 0ustar bmwbmw00000000000000Let's Encrypt Python Client Copyright (c) Electronic Frontier Foundation and others Licensed Apache Version 2.0 The nginx plugin incorporates code from nginxparser Copyright (c) 2014 Fatih Erikli Licensed MIT Text of Apache License ====================== 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. Text of MIT License =================== 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. letsencrypt-0.4.1/setup.cfg0000644000175000017500000000033312665157717015325 0ustar bmwbmw00000000000000[easy_install] zip_ok = false [nosetests] nocapture = 1 cover-package = letsencrypt,acme,letsencrypt_apache,letsencrypt_nginx cover-erase = 1 cover-tests = 1 [egg_info] tag_build = tag_date = 0 tag_svn_revision = 0 letsencrypt-0.4.1/CHANGES.rst0000644000175000017500000000057212665157707015312 0ustar bmwbmw00000000000000ChangeLog ========= Please note: the change log will only get updated after first release - for now please use the `commit log `_. To see the changes in a given release, inspect the github milestone for the release. For instance: https://github.com/letsencrypt/letsencrypt/issues?utf8=%E2%9C%93&q=milestone%3A0.3.0 letsencrypt-0.4.1/PKG-INFO0000644000175000017500000002140612665157717014605 0ustar bmwbmw00000000000000Metadata-Version: 1.1 Name: letsencrypt Version: 0.4.1 Summary: Let's Encrypt client Home-page: https://github.com/letsencrypt/letsencrypt Author: Let's Encrypt Project Author-email: client-dev@letsencrypt.org License: Apache License 2.0 Description: .. notice for github users Disclaimer ========== The Let's Encrypt Client is **BETA SOFTWARE**. It contains plenty of bugs and rough edges, and should be tested thoroughly in staging environments before use on production systems. For more information regarding the status of the project, please see https://letsencrypt.org. Be sure to checkout the `Frequently Asked Questions (FAQ) `_. About the Let's Encrypt Client ============================== The Let's Encrypt Client is a fully-featured, extensible client for the Let's Encrypt CA (or any other CA that speaks the `ACME `_ protocol) that can automate the tasks of obtaining certificates and configuring webservers to use them. Installation ------------ If ``letsencrypt`` is packaged for your OS, you can install it from there, and run it by typing ``letsencrypt``. Because not all operating systems have packages yet, we provide a temporary solution via the ``letsencrypt-auto`` wrapper script, which obtains some dependencies from your OS and puts others in a python virtual environment:: user@webserver:~$ git clone https://github.com/letsencrypt/letsencrypt user@webserver:~$ cd letsencrypt user@webserver:~/letsencrypt$ ./letsencrypt-auto --help Or for full command line help, type:: ./letsencrypt-auto --help all ``letsencrypt-auto`` updates to the latest client release automatically. And since ``letsencrypt-auto`` is a wrapper to ``letsencrypt``, it accepts exactly the same command line flags and arguments. More details about this script and other installation methods can be found `in the User Guide `_. How to run the client --------------------- In many cases, you can just run ``letsencrypt-auto`` or ``letsencrypt``, and the client will guide you through the process of obtaining and installing certs interactively. You can also tell it exactly what you want it to do from the command line. For instance, if you want to obtain a cert for ``example.com``, ``www.example.com``, and ``other.example.net``, using the Apache plugin to both obtain and install the certs, you could do this:: ./letsencrypt-auto --apache -d example.com -d www.example.com -d other.example.net (The first time you run the command, it will make an account, and ask for an email and agreement to the Let's Encrypt Subscriber Agreement; you can automate those with ``--email`` and ``--agree-tos``) If you want to use a webserver that doesn't have full plugin support yet, you can still use "standalone" or "webroot" plugins to obtain a certificate:: ./letsencrypt-auto certonly --standalone --email admin@example.com -d example.com -d www.example.com -d other.example.net Understanding the client in more depth -------------------------------------- To understand what the client is doing in detail, it's important to understand the way it uses plugins. Please see the `explanation of plugins `_ in the User Guide. Links ===== Documentation: https://letsencrypt.readthedocs.org Software project: https://github.com/letsencrypt/letsencrypt Notes for developers: https://letsencrypt.readthedocs.org/en/latest/contributing.html Main Website: https://letsencrypt.org/ IRC Channel: #letsencrypt on `Freenode`_ Community: https://community.letsencrypt.org Mailing list: `client-dev`_ (to subscribe without a Google account, send an email to client-dev+subscribe@letsencrypt.org) |build-status| |coverage| |docs| |container| .. |build-status| image:: https://travis-ci.org/letsencrypt/letsencrypt.svg?branch=master :target: https://travis-ci.org/letsencrypt/letsencrypt :alt: Travis CI status .. |coverage| image:: https://coveralls.io/repos/letsencrypt/letsencrypt/badge.svg?branch=master :target: https://coveralls.io/r/letsencrypt/letsencrypt :alt: Coverage status .. |docs| image:: https://readthedocs.org/projects/letsencrypt/badge/ :target: https://readthedocs.org/projects/letsencrypt/ :alt: Documentation status .. |container| image:: https://quay.io/repository/letsencrypt/letsencrypt/status :target: https://quay.io/repository/letsencrypt/letsencrypt :alt: Docker Repository on Quay.io .. _`installation instructions`: https://letsencrypt.readthedocs.org/en/latest/using.html .. _watch demo video: https://www.youtube.com/watch?v=Gas_sSB-5SU System Requirements =================== The Let's Encrypt Client presently only runs on Unix-ish OSes that include Python 2.6 or 2.7; Python 3.x support will be added after the Public Beta launch. The client requires root access in order to write to ``/etc/letsencrypt``, ``/var/log/letsencrypt``, ``/var/lib/letsencrypt``; to bind to ports 80 and 443 (if you use the ``standalone`` plugin) and to read and modify webserver configurations (if you use the ``apache`` or ``nginx`` plugins). If none of these apply to you, it is theoretically possible to run without root privileges, but for most users who want to avoid running an ACME client as root, either `letsencrypt-nosudo `_ or `simp_le `_ are more appropriate choices. The Apache plugin currently requires a Debian-based OS with augeas version 1.0; this includes Ubuntu 12.04+ and Debian 7+. Current Features ================ * Supports multiple web servers: - apache/2.x (working on Debian 8+ and Ubuntu 12.04+) - standalone (runs its own simple webserver to prove you control a domain) - webroot (adds files to webroot directories in order to prove control of domains and obtain certs) - nginx/0.8.48+ (highly experimental, not included in letsencrypt-auto) * The private key is generated locally on your system. * Can talk to the Let's Encrypt CA or optionally to other ACME compliant services. * Can get domain-validated (DV) certificates. * Can revoke certificates. * Adjustable RSA key bit-length (2048 (default), 4096, ...). * Can optionally install a http -> https redirect, so your site effectively runs https only (Apache only) * Fully automated. * Configuration changes are logged and can be reverted. * Supports ncurses and text (-t) UI, or can be driven entirely from the command line. * Free and Open Source Software, made with Python. .. _Freenode: https://webchat.freenode.net?channels=%23letsencrypt .. _client-dev: https://groups.google.com/a/letsencrypt.org/forum/#!forum/client-dev Platform: UNKNOWN Classifier: Development Status :: 3 - Alpha Classifier: Environment :: Console Classifier: Environment :: Console :: Curses Classifier: Intended Audience :: System Administrators Classifier: License :: OSI Approved :: Apache Software License Classifier: Operating System :: POSIX :: Linux Classifier: Programming Language :: Python Classifier: Programming Language :: Python :: 2 Classifier: Programming Language :: Python :: 2.6 Classifier: Programming Language :: Python :: 2.7 Classifier: Topic :: Internet :: WWW/HTTP Classifier: Topic :: Security Classifier: Topic :: System :: Installation/Setup Classifier: Topic :: System :: Networking Classifier: Topic :: System :: Systems Administration Classifier: Topic :: Utilities letsencrypt-0.4.1/README.rst0000644000175000017500000001474712665157707015210 0ustar bmwbmw00000000000000.. notice for github users Disclaimer ========== The Let's Encrypt Client is **BETA SOFTWARE**. It contains plenty of bugs and rough edges, and should be tested thoroughly in staging environments before use on production systems. For more information regarding the status of the project, please see https://letsencrypt.org. Be sure to checkout the `Frequently Asked Questions (FAQ) `_. About the Let's Encrypt Client ============================== The Let's Encrypt Client is a fully-featured, extensible client for the Let's Encrypt CA (or any other CA that speaks the `ACME `_ protocol) that can automate the tasks of obtaining certificates and configuring webservers to use them. Installation ------------ If ``letsencrypt`` is packaged for your OS, you can install it from there, and run it by typing ``letsencrypt``. Because not all operating systems have packages yet, we provide a temporary solution via the ``letsencrypt-auto`` wrapper script, which obtains some dependencies from your OS and puts others in a python virtual environment:: user@webserver:~$ git clone https://github.com/letsencrypt/letsencrypt user@webserver:~$ cd letsencrypt user@webserver:~/letsencrypt$ ./letsencrypt-auto --help Or for full command line help, type:: ./letsencrypt-auto --help all ``letsencrypt-auto`` updates to the latest client release automatically. And since ``letsencrypt-auto`` is a wrapper to ``letsencrypt``, it accepts exactly the same command line flags and arguments. More details about this script and other installation methods can be found `in the User Guide `_. How to run the client --------------------- In many cases, you can just run ``letsencrypt-auto`` or ``letsencrypt``, and the client will guide you through the process of obtaining and installing certs interactively. You can also tell it exactly what you want it to do from the command line. For instance, if you want to obtain a cert for ``example.com``, ``www.example.com``, and ``other.example.net``, using the Apache plugin to both obtain and install the certs, you could do this:: ./letsencrypt-auto --apache -d example.com -d www.example.com -d other.example.net (The first time you run the command, it will make an account, and ask for an email and agreement to the Let's Encrypt Subscriber Agreement; you can automate those with ``--email`` and ``--agree-tos``) If you want to use a webserver that doesn't have full plugin support yet, you can still use "standalone" or "webroot" plugins to obtain a certificate:: ./letsencrypt-auto certonly --standalone --email admin@example.com -d example.com -d www.example.com -d other.example.net Understanding the client in more depth -------------------------------------- To understand what the client is doing in detail, it's important to understand the way it uses plugins. Please see the `explanation of plugins `_ in the User Guide. Links ===== Documentation: https://letsencrypt.readthedocs.org Software project: https://github.com/letsencrypt/letsencrypt Notes for developers: https://letsencrypt.readthedocs.org/en/latest/contributing.html Main Website: https://letsencrypt.org/ IRC Channel: #letsencrypt on `Freenode`_ Community: https://community.letsencrypt.org Mailing list: `client-dev`_ (to subscribe without a Google account, send an email to client-dev+subscribe@letsencrypt.org) |build-status| |coverage| |docs| |container| .. |build-status| image:: https://travis-ci.org/letsencrypt/letsencrypt.svg?branch=master :target: https://travis-ci.org/letsencrypt/letsencrypt :alt: Travis CI status .. |coverage| image:: https://coveralls.io/repos/letsencrypt/letsencrypt/badge.svg?branch=master :target: https://coveralls.io/r/letsencrypt/letsencrypt :alt: Coverage status .. |docs| image:: https://readthedocs.org/projects/letsencrypt/badge/ :target: https://readthedocs.org/projects/letsencrypt/ :alt: Documentation status .. |container| image:: https://quay.io/repository/letsencrypt/letsencrypt/status :target: https://quay.io/repository/letsencrypt/letsencrypt :alt: Docker Repository on Quay.io .. _`installation instructions`: https://letsencrypt.readthedocs.org/en/latest/using.html .. _watch demo video: https://www.youtube.com/watch?v=Gas_sSB-5SU System Requirements =================== The Let's Encrypt Client presently only runs on Unix-ish OSes that include Python 2.6 or 2.7; Python 3.x support will be added after the Public Beta launch. The client requires root access in order to write to ``/etc/letsencrypt``, ``/var/log/letsencrypt``, ``/var/lib/letsencrypt``; to bind to ports 80 and 443 (if you use the ``standalone`` plugin) and to read and modify webserver configurations (if you use the ``apache`` or ``nginx`` plugins). If none of these apply to you, it is theoretically possible to run without root privileges, but for most users who want to avoid running an ACME client as root, either `letsencrypt-nosudo `_ or `simp_le `_ are more appropriate choices. The Apache plugin currently requires a Debian-based OS with augeas version 1.0; this includes Ubuntu 12.04+ and Debian 7+. Current Features ================ * Supports multiple web servers: - apache/2.x (working on Debian 8+ and Ubuntu 12.04+) - standalone (runs its own simple webserver to prove you control a domain) - webroot (adds files to webroot directories in order to prove control of domains and obtain certs) - nginx/0.8.48+ (highly experimental, not included in letsencrypt-auto) * The private key is generated locally on your system. * Can talk to the Let's Encrypt CA or optionally to other ACME compliant services. * Can get domain-validated (DV) certificates. * Can revoke certificates. * Adjustable RSA key bit-length (2048 (default), 4096, ...). * Can optionally install a http -> https redirect, so your site effectively runs https only (Apache only) * Fully automated. * Configuration changes are logged and can be reverted. * Supports ncurses and text (-t) UI, or can be driven entirely from the command line. * Free and Open Source Software, made with Python. .. _Freenode: https://webchat.freenode.net?channels=%23letsencrypt .. _client-dev: https://groups.google.com/a/letsencrypt.org/forum/#!forum/client-dev