├── docs ├── _static │ └── .gitignore ├── .gitignore ├── _templates │ └── .gitignore ├── man │ └── jws.rst ├── api │ ├── client.rst │ ├── errors.rst │ ├── fields.rst │ ├── messages.rst │ ├── challenges.rst │ ├── standalone.rst │ └── jose.rst ├── api.rst ├── jws-help.txt ├── index.rst ├── Makefile ├── make.bat └── conf.py ├── acme.egg-info ├── top_level.txt ├── dependency_links.txt ├── requires.txt ├── PKG-INFO └── SOURCES.txt ├── debian ├── source │ └── format ├── python-acme-doc.docs ├── pydist-overrides ├── python-acme-doc.links ├── watch ├── python-acme-doc.doc-base ├── rules ├── copyright ├── upstream │ └── signing-key.asc ├── control └── changelog ├── README.rst ├── pytest.ini ├── examples ├── standalone │ └── README └── http01_example.py ├── tests ├── testdata │ ├── cert.der │ ├── csr.der │ ├── cert-nocn.der │ ├── rsa256_key.pem │ ├── csr-nosans.pem │ ├── rsa512_key.pem │ ├── README │ ├── csr.pem │ ├── csr-san.pem │ ├── csr-6sans.pem │ ├── dsa512_key.pem │ ├── cert.pem │ ├── cert-san.pem │ ├── rsa1024_key.pem │ ├── rsa2048_cert.pem │ ├── critical-san.pem │ ├── csr-idnsans.pem │ ├── rsa2048_key.pem │ ├── cert-idnsans.pem │ ├── csr-100sans.pem │ └── cert-100sans.pem ├── util_test.py ├── magic_typing_test.py ├── errors_test.py ├── jose_test.py ├── test_util.py ├── jws_test.py ├── fields_test.py ├── standalone_test.py ├── crypto_util_test.py ├── challenges_test.py └── messages_test.py ├── setup.cfg ├── acme ├── util.py ├── magic_typing.py ├── __init__.py ├── fields.py ├── jws.py ├── errors.py ├── standalone.py ├── crypto_util.py └── challenges.py ├── MANIFEST.in ├── PKG-INFO ├── setup.py └── LICENSE.txt /docs/_static/.gitignore: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/.gitignore: -------------------------------------------------------------------------------- 1 | /_build/ 2 | -------------------------------------------------------------------------------- /docs/_templates/.gitignore: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /acme.egg-info/top_level.txt: -------------------------------------------------------------------------------- 1 | acme 2 | -------------------------------------------------------------------------------- /debian/source/format: -------------------------------------------------------------------------------- 1 | 3.0 (quilt) 2 | -------------------------------------------------------------------------------- /acme.egg-info/dependency_links.txt: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | ACME protocol implementation in Python 2 | -------------------------------------------------------------------------------- /debian/python-acme-doc.docs: -------------------------------------------------------------------------------- 1 | build/html 2 | examples 3 | -------------------------------------------------------------------------------- /docs/man/jws.rst: -------------------------------------------------------------------------------- 1 | .. literalinclude:: ../jws-help.txt 2 | -------------------------------------------------------------------------------- /debian/pydist-overrides: -------------------------------------------------------------------------------- 1 | setuptools python-setuptools (>= 11.3~) 2 | -------------------------------------------------------------------------------- /debian/python-acme-doc.links: -------------------------------------------------------------------------------- 1 | usr/share/doc/python-acme-doc usr/share/doc/python-acme -------------------------------------------------------------------------------- /pytest.ini: -------------------------------------------------------------------------------- 1 | [pytest] 2 | norecursedirs = .* build dist CVS _darcs {arch} *.egg 3 | -------------------------------------------------------------------------------- /docs/api/client.rst: -------------------------------------------------------------------------------- 1 | Client 2 | ------ 3 | 4 | .. automodule:: acme.client 5 | :members: 6 | -------------------------------------------------------------------------------- /docs/api/errors.rst: -------------------------------------------------------------------------------- 1 | Errors 2 | ------ 3 | 4 | .. automodule:: acme.errors 5 | :members: 6 | -------------------------------------------------------------------------------- /docs/api/fields.rst: -------------------------------------------------------------------------------- 1 | Fields 2 | ------ 3 | 4 | .. automodule:: acme.fields 5 | :members: 6 | -------------------------------------------------------------------------------- /docs/api/messages.rst: -------------------------------------------------------------------------------- 1 | Messages 2 | -------- 3 | 4 | .. automodule:: acme.messages 5 | :members: 6 | -------------------------------------------------------------------------------- /examples/standalone/README: -------------------------------------------------------------------------------- 1 | python -m acme.standalone -p 1234 2 | curl -k https://localhost:1234 3 | -------------------------------------------------------------------------------- /tests/testdata/cert.der: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/norbusan/acme-debian/HEAD/tests/testdata/cert.der -------------------------------------------------------------------------------- /tests/testdata/csr.der: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/norbusan/acme-debian/HEAD/tests/testdata/csr.der -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [bdist_wheel] 2 | universal = 1 3 | 4 | [egg_info] 5 | tag_build = 6 | tag_date = 0 7 | 8 | -------------------------------------------------------------------------------- /docs/api/challenges.rst: -------------------------------------------------------------------------------- 1 | Challenges 2 | ---------- 3 | 4 | .. automodule:: acme.challenges 5 | :members: 6 | -------------------------------------------------------------------------------- /docs/api/standalone.rst: -------------------------------------------------------------------------------- 1 | Standalone 2 | ---------- 3 | 4 | .. automodule:: acme.standalone 5 | :members: 6 | -------------------------------------------------------------------------------- /tests/testdata/cert-nocn.der: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/norbusan/acme-debian/HEAD/tests/testdata/cert-nocn.der -------------------------------------------------------------------------------- /docs/api.rst: -------------------------------------------------------------------------------- 1 | ================= 2 | API Documentation 3 | ================= 4 | 5 | .. toctree:: 6 | :glob: 7 | 8 | api/* 9 | -------------------------------------------------------------------------------- /debian/watch: -------------------------------------------------------------------------------- 1 | version=3 2 | opts=uversionmangle=s/(rc|a|b|c)/~$1/,pgpsigurlmangle=s/$/.asc/ \ 3 | https://pypi.debian.net/acme/acme-(.+)\.(?:zip|tgz|tbz|txz|(?:tar\.(?:gz|bz2|xz))) 4 | -------------------------------------------------------------------------------- /acme/util.py: -------------------------------------------------------------------------------- 1 | """ACME utilities.""" 2 | import six 3 | 4 | 5 | def map_keys(dikt, func): 6 | """Map dictionary keys.""" 7 | return dict((func(key), value) for key, value in six.iteritems(dikt)) 8 | -------------------------------------------------------------------------------- /docs/api/jose.rst: -------------------------------------------------------------------------------- 1 | JOSE 2 | ---- 3 | 4 | The ``acme.jose`` module was moved to its own package "josepy_". 5 | Please refer to its documentation there. 6 | 7 | .. _josepy: https://josepy.readthedocs.io/ 8 | -------------------------------------------------------------------------------- /docs/jws-help.txt: -------------------------------------------------------------------------------- 1 | usage: jws [-h] [--compact] {sign,verify} ... 2 | 3 | positional arguments: 4 | {sign,verify} 5 | 6 | optional arguments: 7 | -h, --help show this help message and exit 8 | --compact 9 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include LICENSE.txt 2 | include README.rst 3 | include pytest.ini 4 | recursive-include docs * 5 | recursive-include examples * 6 | recursive-include tests * 7 | global-exclude __pycache__ 8 | global-exclude *.py[cod] 9 | -------------------------------------------------------------------------------- /acme.egg-info/requires.txt: -------------------------------------------------------------------------------- 1 | cryptography>=1.2.3 2 | josepy>=1.1.0 3 | mock 4 | PyOpenSSL>=0.13.1 5 | pyrfc3339 6 | pytz 7 | requests[security]>=2.6.0 8 | requests-toolbelt>=0.3.0 9 | setuptools 10 | six>=1.9.0 11 | 12 | [dev] 13 | pytest 14 | pytest-xdist 15 | tox 16 | 17 | [docs] 18 | Sphinx>=1.0 19 | sphinx_rtd_theme 20 | -------------------------------------------------------------------------------- /tests/testdata/rsa256_key.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | MIGrAgEAAiEAm2Fylv+Uz7trgTW8EBHP3FQSMeZs2GNQ6VRo1sIVJEkCAwEAAQIh 3 | AJT0BA/xD01dFCAXzSNyj9nfSZa3NpqzJZZn/eOm7vghAhEAzUVNZn4lLLBD1R6N 4 | E8TKNQIRAMHHyn3O5JeY36lwKwkUlEUCEAliRauN0L0+QZuYjfJ9aJECEGx4dru3 5 | rTPCyighdqWNlHUCEQCiLjlwSRtWgmMBudCkVjzt 6 | -----END RSA PRIVATE KEY----- 7 | -------------------------------------------------------------------------------- /debian/python-acme-doc.doc-base: -------------------------------------------------------------------------------- 1 | Document: python-acme-doc 2 | Title: Documentation for Python's ACME module 3 | Author: Let's Encrypt Team 4 | Abstract: These HTML documentation contain the auto-generated 5 | documentation for the acme module in Python. 6 | Section: Programming/Python 7 | 8 | Format: HTML 9 | Index: /usr/share/doc/python-acme-doc/html/index.html 10 | Files: /usr/share/doc/python-acme-doc/html/*.html 11 | -------------------------------------------------------------------------------- /debian/rules: -------------------------------------------------------------------------------- 1 | #!/usr/bin/make -f 2 | 3 | export PYBUILD_NAME = acme 4 | 5 | %: 6 | dh $@ --with python3,sphinxdoc --buildsystem=pybuild 7 | 8 | override_dh_auto_build: 9 | dh_auto_build 10 | PYTHONPATH=. \ 11 | http_proxy='127.0.0.1:9' \ 12 | https_proxy='127.0.0.1:9' \ 13 | sphinx-build -N -bhtml docs/ build/html 14 | 15 | override_dh_auto_install: 16 | dh_auto_install 17 | find $(CURDIR)/debian/ -type d -name testdata -print0 | xargs -0 rm -rf '{}' \; 18 | -------------------------------------------------------------------------------- /tests/testdata/csr-nosans.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE REQUEST----- 2 | MIIBFTCBwAIBADBbMQswCQYDVQQGEwJBVTETMBEGA1UECAwKU29tZS1TdGF0ZTEh 3 | MB8GA1UECgwYSW50ZXJuZXQgV2lkZ2l0cyBQdHkgTHRkMRQwEgYDVQQDDAtleGFt 4 | cGxlLm9yZzBcMA0GCSqGSIb3DQEBAQUAA0sAMEgCQQD0thFxUTc2v6qV55wRxfwn 5 | BUOeN4bVfu5ywJqy65kzR7T1yZi5TPEiQyM7/3HgBVy9ddFc8RX4vNZaR+ROXNEz 6 | AgMBAAGgADANBgkqhkiG9w0BAQsFAANBAMikGL8Ch7hQCStXH7chhDp6+pt2+VSo 7 | wgsrPQ2Bw4veDMlSemUrH+4e0TwbbntHfvXTDHWs9P3BiIDJLxFrjuA= 8 | -----END CERTIFICATE REQUEST----- 9 | -------------------------------------------------------------------------------- /tests/util_test.py: -------------------------------------------------------------------------------- 1 | """Tests for acme.util.""" 2 | import unittest 3 | 4 | 5 | class MapKeysTest(unittest.TestCase): 6 | """Tests for acme.util.map_keys.""" 7 | 8 | def test_it(self): 9 | from acme.util import map_keys 10 | self.assertEqual({'a': 'b', 'c': 'd'}, 11 | map_keys({'a': 'b', 'c': 'd'}, lambda key: key)) 12 | self.assertEqual({2: 2, 4: 4}, map_keys({1: 2, 3: 4}, lambda x: x + 1)) 13 | 14 | 15 | if __name__ == '__main__': 16 | unittest.main() # pragma: no cover 17 | -------------------------------------------------------------------------------- /tests/testdata/rsa512_key.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | MIIBOgIBAAJBAKx1c7RR7R/drnBSQ/zfx1vQLHUbFLh1AQQQ5R8DZUXd36efNK79 3 | vukFhN9HFoHZiUvOjm0c+pVE6K+EdE/twuUCAwEAAQJAMbrEnJCrQe8YqAbw1/Bn 4 | elAzIamndfE3U8bTavf9sgFpS4HL83rhd6PDbvx81ucaJAT/5x048fM/nFl4fzAc 5 | mQIhAOF/a9o3EIsDKEmUl+Z1OaOiUxDF3kqWSmALEsmvDhwXAiEAw8ljV5RO/rUp 6 | Zu2YMDFq3MKpyyMgBIJ8CxmGRc6gCmMCIGRQzkcmhfqBrhOFwkmozrqIBRIKJIjj 7 | 8TRm2LXWZZ2DAiAqVO7PztdNpynugUy4jtbGKKjBrTSNBRGA7OHlUgm0dQIhALQq 8 | 6oGU29Vxlvt3k0vmiRKU4AVfLyNXIGtcWcNG46h/ 9 | -----END RSA PRIVATE KEY----- 10 | -------------------------------------------------------------------------------- /tests/testdata/README: -------------------------------------------------------------------------------- 1 | In order for acme.test_util._guess_loader to work properly, make sure 2 | to use appropriate extension for vector filenames: .pem for PEM and 3 | .der for DER. 4 | 5 | The following command has been used to generate test keys: 6 | 7 | for x in 256 512 1024 2048; do openssl genrsa -out rsa${k}_key.pem $k; done 8 | 9 | and for the CSR: 10 | 11 | openssl req -key rsa2048_key.pem -new -subj '/CN=example.com' -outform DER > csr.der 12 | 13 | and for the certificate: 14 | 15 | openssl req -key rsa2047_key.pem -new -subj '/CN=example.com' -x509 -outform DER > cert.der 16 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | .. acme-python documentation master file, created by 2 | sphinx-quickstart on Sun Oct 18 13:38:06 2015. 3 | You can adapt this file completely to your liking, but it should at least 4 | contain the root `toctree` directive. 5 | 6 | Welcome to acme-python's documentation! 7 | ======================================= 8 | 9 | Contents: 10 | 11 | .. toctree:: 12 | :maxdepth: 2 13 | 14 | api 15 | 16 | .. automodule:: acme 17 | :members: 18 | 19 | Indices and tables 20 | ================== 21 | 22 | * :ref:`genindex` 23 | * :ref:`modindex` 24 | * :ref:`search` 25 | 26 | -------------------------------------------------------------------------------- /tests/testdata/csr.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE REQUEST----- 2 | MIIBXTCCAQcCAQAweTELMAkGA1UEBhMCVVMxETAPBgNVBAgMCE1pY2hpZ2FuMRIw 3 | EAYDVQQHDAlBbm4gQXJib3IxDDAKBgNVBAoMA0VGRjEfMB0GA1UECwwWVW5pdmVy 4 | c2l0eSBvZiBNaWNoaWdhbjEUMBIGA1UEAwwLZXhhbXBsZS5jb20wXDANBgkqhkiG 5 | 9w0BAQEFAANLADBIAkEArHVztFHtH92ucFJD/N/HW9AsdRsUuHUBBBDlHwNlRd3f 6 | p580rv2+6QWE30cWgdmJS86ObRz6lUTor4R0T+3C5QIDAQABoCkwJwYJKoZIhvcN 7 | AQkOMRowGDAWBgNVHREEDzANggtleGFtcGxlLmNvbTANBgkqhkiG9w0BAQsFAANB 8 | AHJH/O6BtC9aGzEVCMGOZ7z9iIRHWSzr9x/bOzn7hLwsbXPAgO1QxEwL+X+4g20G 9 | n9XBE1N9W6HCIEut2d8wACg= 10 | -----END CERTIFICATE REQUEST----- 11 | -------------------------------------------------------------------------------- /acme/magic_typing.py: -------------------------------------------------------------------------------- 1 | """Shim class to not have to depend on typing module in prod.""" 2 | import sys 3 | 4 | 5 | class TypingClass(object): 6 | """Ignore import errors by getting anything""" 7 | def __getattr__(self, name): 8 | return None 9 | 10 | try: 11 | # mypy doesn't respect modifying sys.modules 12 | from typing import * # pylint: disable=wildcard-import, unused-wildcard-import 13 | # pylint: disable=unused-import 14 | from typing import Collection, IO # type: ignore 15 | # pylint: enable=unused-import 16 | except ImportError: 17 | sys.modules[__name__] = TypingClass() 18 | -------------------------------------------------------------------------------- /tests/testdata/csr-san.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE REQUEST----- 2 | MIIBbjCCARgCAQAweTELMAkGA1UEBhMCVVMxETAPBgNVBAgMCE1pY2hpZ2FuMRIw 3 | EAYDVQQHDAlBbm4gQXJib3IxDDAKBgNVBAoMA0VGRjEfMB0GA1UECwwWVW5pdmVy 4 | c2l0eSBvZiBNaWNoaWdhbjEUMBIGA1UEAwwLZXhhbXBsZS5jb20wXDANBgkqhkiG 5 | 9w0BAQEFAANLADBIAkEArHVztFHtH92ucFJD/N/HW9AsdRsUuHUBBBDlHwNlRd3f 6 | p580rv2+6QWE30cWgdmJS86ObRz6lUTor4R0T+3C5QIDAQABoDowOAYJKoZIhvcN 7 | AQkOMSswKTAnBgNVHREEIDAeggtleGFtcGxlLmNvbYIPd3d3LmV4YW1wbGUuY29t 8 | MA0GCSqGSIb3DQEBCwUAA0EAZGBM8J1rRs7onFgtc76mOeoT1c3v0ZsEmxQfb2Wy 9 | tmReY6X1N4cs38D9VSow+VMRu2LWkKvzS7RUFSaTaeQz1A== 10 | -----END CERTIFICATE REQUEST----- 11 | -------------------------------------------------------------------------------- /tests/testdata/csr-6sans.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE REQUEST----- 2 | MIIBuzCCAWUCAQAweTELMAkGA1UEBhMCVVMxETAPBgNVBAgTCE1pY2hpZ2FuMRIw 3 | EAYDVQQHEwlBbm4gQXJib3IxDDAKBgNVBAoTA0VGRjEfMB0GA1UECxMWVW5pdmVy 4 | c2l0eSBvZiBNaWNoaWdhbjEUMBIGA1UEAxMLZXhhbXBsZS5jb20wXDANBgkqhkiG 5 | 9w0BAQEFAANLADBIAkEA9LYRcVE3Nr+qleecEcX8JwVDnjeG1X7ucsCasuuZM0e0 6 | 9cmYuUzxIkMjO/9x4AVcvXXRXPEV+LzWWkfkTlzRMwIDAQABoIGGMIGDBgkqhkiG 7 | 9w0BCQ4xdjB0MHIGA1UdEQRrMGmCC2V4YW1wbGUuY29tggtleGFtcGxlLm9yZ4IL 8 | ZXhhbXBsZS5uZXSCDGV4YW1wbGUuaW5mb4IVc3ViZG9tYWluLmV4YW1wbGUuY29t 9 | ghtvdGhlci5zdWJkb21haW4uZXhhbXBsZS5jb20wDQYJKoZIhvcNAQELBQADQQBd 10 | k4BE5qvEvkYoZM/2++Xd9RrQ6wsdj0QiJQCozfsI4lQx6ZJnbtNc7HpDrX4W6XIv 11 | IvzVBz/nD11drfz/RNuX 12 | -----END CERTIFICATE REQUEST----- 13 | -------------------------------------------------------------------------------- /tests/testdata/dsa512_key.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN DSA PARAMETERS----- 2 | MIGdAkEAwebEoGBfokKQeALHHnAZMQwYU35ILEBdV8oUmzv7qpSVUoHihyqfn6GC 3 | OixAKSP8EJYcTilIqPbFbfFyOPlbLwIVANoFHEDiQgknAvKrG78pHzAJdQSPAkEA 4 | qfka5Bnl+CeEMpzVZGrOVqZE/LFdZK9eT6YtWjzqtIkf3hwXUVxJsTnBG4xmrfvl 5 | 41pgNJpgu99YOYqPpS0g7A== 6 | -----END DSA PARAMETERS----- 7 | -----BEGIN DSA PRIVATE KEY----- 8 | MIH5AgEAAkEAwebEoGBfokKQeALHHnAZMQwYU35ILEBdV8oUmzv7qpSVUoHihyqf 9 | n6GCOixAKSP8EJYcTilIqPbFbfFyOPlbLwIVANoFHEDiQgknAvKrG78pHzAJdQSP 10 | AkEAqfka5Bnl+CeEMpzVZGrOVqZE/LFdZK9eT6YtWjzqtIkf3hwXUVxJsTnBG4xm 11 | rfvl41pgNJpgu99YOYqPpS0g7AJATQ2LUzjGQSM6UljcPY5I2OD9THkUR9kH2tth 12 | zZd70UoI9btrVaTizgqYShuok94glSQNK0H92JgUk3scJPaAkAIVAMDn61h6vrCE 13 | mNv063So6E+eYaIN 14 | -----END DSA PRIVATE KEY----- 15 | -------------------------------------------------------------------------------- /tests/testdata/cert.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIB3jCCAYigAwIBAgICBTkwDQYJKoZIhvcNAQELBQAwdzELMAkGA1UEBhMCVVMx 3 | ETAPBgNVBAgMCE1pY2hpZ2FuMRIwEAYDVQQHDAlBbm4gQXJib3IxKzApBgNVBAoM 4 | IlVuaXZlcnNpdHkgb2YgTWljaGlnYW4gYW5kIHRoZSBFRkYxFDASBgNVBAMMC2V4 5 | YW1wbGUuY29tMB4XDTE0MTIxMTIyMzQ0NVoXDTE0MTIxODIyMzQ0NVowdzELMAkG 6 | A1UEBhMCVVMxETAPBgNVBAgMCE1pY2hpZ2FuMRIwEAYDVQQHDAlBbm4gQXJib3Ix 7 | KzApBgNVBAoMIlVuaXZlcnNpdHkgb2YgTWljaGlnYW4gYW5kIHRoZSBFRkYxFDAS 8 | BgNVBAMMC2V4YW1wbGUuY29tMFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBAKx1c7RR 9 | 7R/drnBSQ/zfx1vQLHUbFLh1AQQQ5R8DZUXd36efNK79vukFhN9HFoHZiUvOjm0c 10 | +pVE6K+EdE/twuUCAwEAATANBgkqhkiG9w0BAQsFAANBAC24z0IdwIVKSlntksll 11 | vr6zJepBH5fMndfk3XJp10jT6VE+14KNtjh02a56GoraAvJAT5/H67E8GvJ/ocNn 12 | B/o= 13 | -----END CERTIFICATE----- 14 | -------------------------------------------------------------------------------- /acme/__init__.py: -------------------------------------------------------------------------------- 1 | """ACME protocol implementation. 2 | 3 | This module is an implementation of the `ACME protocol`_. 4 | 5 | .. _`ACME protocol`: https://ietf-wg-acme.github.io/acme 6 | 7 | """ 8 | import sys 9 | import warnings 10 | 11 | # This code exists to keep backwards compatibility with people using acme.jose 12 | # before it became the standalone josepy package. 13 | # 14 | # It is based on 15 | # https://github.com/requests/requests/blob/1278ecdf71a312dc2268f3bfc0aabfab3c006dcf/requests/packages.py 16 | import josepy as jose 17 | 18 | for mod in list(sys.modules): 19 | # This traversal is apparently necessary such that the identities are 20 | # preserved (acme.jose.* is josepy.*) 21 | if mod == 'josepy' or mod.startswith('josepy.'): 22 | sys.modules['acme.' + mod.replace('josepy', 'jose', 1)] = sys.modules[mod] 23 | -------------------------------------------------------------------------------- /tests/testdata/cert-san.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIICFjCCAcCgAwIBAgICBTkwDQYJKoZIhvcNAQELBQAwdzELMAkGA1UEBhMCVVMx 3 | ETAPBgNVBAgMCE1pY2hpZ2FuMRIwEAYDVQQHDAlBbm4gQXJib3IxKzApBgNVBAoM 4 | IlVuaXZlcnNpdHkgb2YgTWljaGlnYW4gYW5kIHRoZSBFRkYxFDASBgNVBAMMC2V4 5 | YW1wbGUuY29tMB4XDTE0MTIxMTIyMzQ0NVoXDTE0MTIxODIyMzQ0NVowdzELMAkG 6 | A1UEBhMCVVMxETAPBgNVBAgMCE1pY2hpZ2FuMRIwEAYDVQQHDAlBbm4gQXJib3Ix 7 | KzApBgNVBAoMIlVuaXZlcnNpdHkgb2YgTWljaGlnYW4gYW5kIHRoZSBFRkYxFDAS 8 | BgNVBAMMC2V4YW1wbGUuY29tMFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBAKx1c7RR 9 | 7R/drnBSQ/zfx1vQLHUbFLh1AQQQ5R8DZUXd36efNK79vukFhN9HFoHZiUvOjm0c 10 | +pVE6K+EdE/twuUCAwEAAaM2MDQwCQYDVR0TBAIwADAnBgNVHREEIDAeggtleGFt 11 | cGxlLmNvbYIPd3d3LmV4YW1wbGUuY29tMA0GCSqGSIb3DQEBCwUAA0EASuvNKFTF 12 | nTJsvnSXn52f4BMZJJ2id/kW7+r+FJRm+L20gKQ1aqq8d3e/lzRUrv5SMf1TAOe7 13 | RDjyGMKy5ZgM2w== 14 | -----END CERTIFICATE----- 15 | -------------------------------------------------------------------------------- /tests/testdata/rsa1024_key.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | MIICXAIBAAKBgQCaifO0fGlcAcjjcYEAPYcIL0Hf0KiNa9VCJ14RBdlZxLWRrVFi 3 | 4tdNCKSKqzKuKrrA8DWd4PHFD7UpLyRrPPXY6GozAyCT+5UFBClGJ2KyNKu+eU6/ 4 | w4C1kpO4lpeXs8ptFc1lA9P8V1M/MkWzTE402nPNK0uUmZNo2tsFpGJUSQIDAQAB 5 | AoGAFjLWxQhSAhtnhfRZ+XTdHrnbFpFchOQGgDgzdPKIJDLzefeRh0jacIBbUmgB 6 | Ia+Vn/1hVkpnsEzvUvkonBbnoYWlYVQdpNTmrrew7SOztf8/1fYCsSkyDAvqGTXc 7 | TmHM0PaLS+junoWcKOvQRVb0N3k+43OnBkr2b393Sx30qGECQQDNO2IBWOsYs8cB 8 | CZQAZs8zBlbwBFZibqovqpLwXt9adBIsT9XzgagGbJMpzSuoHTUn3QqqJd9uHD8X 9 | UTmmoh4NAkEAwMRauo+PlNj8W1lusflko52KL17+E5cmeOERM2xvhZNpO7d3/1ak 10 | Co9dxVMicrYSh7jXbcXFNt3xNDTv6Dg8LQJAPuJwMDt/pc0IMCAwMkNOP7M0lkyt 11 | 73E7QmnAplhblcq0+tDnnLpgsr84BHnyY4u3iuRm7SW3pXSQPGPOB2nrTQJANBXa 12 | HgakWSe4KEal7ljgpITwzZPxOwHgV1EZALgP+hu2l3gfaFLUyDWstKCd8jjYEOwU 13 | 6YhCnWyiu+SB3lEzkQJBAJapJpfypFyY8kQNYlYILLBcPu5fmy3QUZKHJ4L3rIVJ 14 | c2UTLMeBBgGFHT04CtWntmjwzSv+V6lwiCxKXsIUySc= 15 | -----END RSA PRIVATE KEY----- 16 | -------------------------------------------------------------------------------- /PKG-INFO: -------------------------------------------------------------------------------- 1 | Metadata-Version: 2.1 2 | Name: acme 3 | Version: 1.1.0 4 | Summary: ACME protocol implementation in Python 5 | Home-page: https://github.com/letsencrypt/letsencrypt 6 | Author: Certbot Project 7 | Author-email: client-dev@letsencrypt.org 8 | License: Apache License 2.0 9 | Description: UNKNOWN 10 | Platform: UNKNOWN 11 | Classifier: Development Status :: 5 - Production/Stable 12 | Classifier: Intended Audience :: Developers 13 | Classifier: License :: OSI Approved :: Apache Software License 14 | Classifier: Programming Language :: Python 15 | Classifier: Programming Language :: Python :: 2 16 | Classifier: Programming Language :: Python :: 2.7 17 | Classifier: Programming Language :: Python :: 3 18 | Classifier: Programming Language :: Python :: 3.4 19 | Classifier: Programming Language :: Python :: 3.5 20 | Classifier: Programming Language :: Python :: 3.6 21 | Classifier: Programming Language :: Python :: 3.7 22 | Classifier: Programming Language :: Python :: 3.8 23 | Classifier: Topic :: Internet :: WWW/HTTP 24 | Classifier: Topic :: Security 25 | Requires-Python: >=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.* 26 | Provides-Extra: docs 27 | Provides-Extra: dev 28 | -------------------------------------------------------------------------------- /acme.egg-info/PKG-INFO: -------------------------------------------------------------------------------- 1 | Metadata-Version: 2.1 2 | Name: acme 3 | Version: 1.1.0 4 | Summary: ACME protocol implementation in Python 5 | Home-page: https://github.com/letsencrypt/letsencrypt 6 | Author: Certbot Project 7 | Author-email: client-dev@letsencrypt.org 8 | License: Apache License 2.0 9 | Description: UNKNOWN 10 | Platform: UNKNOWN 11 | Classifier: Development Status :: 5 - Production/Stable 12 | Classifier: Intended Audience :: Developers 13 | Classifier: License :: OSI Approved :: Apache Software License 14 | Classifier: Programming Language :: Python 15 | Classifier: Programming Language :: Python :: 2 16 | Classifier: Programming Language :: Python :: 2.7 17 | Classifier: Programming Language :: Python :: 3 18 | Classifier: Programming Language :: Python :: 3.4 19 | Classifier: Programming Language :: Python :: 3.5 20 | Classifier: Programming Language :: Python :: 3.6 21 | Classifier: Programming Language :: Python :: 3.7 22 | Classifier: Programming Language :: Python :: 3.8 23 | Classifier: Topic :: Internet :: WWW/HTTP 24 | Classifier: Topic :: Security 25 | Requires-Python: >=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.* 26 | Provides-Extra: docs 27 | Provides-Extra: dev 28 | -------------------------------------------------------------------------------- /debian/copyright: -------------------------------------------------------------------------------- 1 | Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/ 2 | Source: https://pypi.python.org/pypi/acme/ 3 | Upstream-Name: acme 4 | 5 | Files: * 6 | Copyright: 2015, Electronic Frontier Foundation and others 7 | License: Apache-2.0 8 | 9 | Files: debian/* 10 | Copyright: 2015-2018, Harlan Lieberman-Berg 11 | 2015, Francois Marier 12 | License: Apache-2.0 13 | 14 | License: Apache-2.0 15 | Licensed under the Apache License, Version 2.0 (the "License"); you 16 | may not use this file except in compliance with the License. You may 17 | obtain a copy of the License at 18 | . 19 | http://www.apache.org/licenses/LICENSE-2.0 20 | . 21 | Unless required by applicable law or agreed to in writing, software 22 | distributed under the License is distributed on an "AS IS" BASIS, 23 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 24 | implied. See the License for the specific language governing 25 | permissions and limitations under the License. 26 | . 27 | On Debian systems, the complete text of the Apache version 2.0 28 | license can be found in "/usr/share/common-licenses/Apache-2.0". 29 | -------------------------------------------------------------------------------- /tests/testdata/rsa2048_cert.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIDjjCCAnagAwIBAgIJALVG/VbBb5U7MA0GCSqGSIb3DQEBCwUAMFsxCzAJBgNV 3 | BAYTAkFVMQswCQYDVQQIDAJXQTEeMBwGA1UEBwwVVGhlIG1pZGRsZSBvZiBub3do 4 | ZXJlMR8wHQYDVQQKDBZDZXJ0Ym90IFRlc3QgQ2VydHMgSW5jMCAXDTE2MTEyODIx 5 | MzUzN1oYDzIyOTAwOTEzMjEzNTM3WjBbMQswCQYDVQQGEwJBVTELMAkGA1UECAwC 6 | V0ExHjAcBgNVBAcMFVRoZSBtaWRkbGUgb2Ygbm93aGVyZTEfMB0GA1UECgwWQ2Vy 7 | dGJvdCBUZXN0IENlcnRzIEluYzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoC 8 | ggEBANoVT1pdvRUUBOqvm7M2ebLEHV7higUH7qAGUZEkfP6W4YriYVY+IHrH1svN 9 | PSa+oPTK7weDNmT11ehWnGyECIM9z2r2Hi9yVV0ycxh4hWQ4Nt8BAKZwCwaXpyWm 10 | 7Gj6m2EzpSN5Dd67g5YAQBrUUh1+RRbFi9c0Ls/6ZOExMvfg8kqt4c2sXCgH1IFn 11 | xvvOjBYop7xh0x3L1Akyax0tw8qgQp/z5mkupmVDNJYPFmbzFPMNyDR61ed6QUTD 12 | g7P4UAuFkejLLzFvz5YaO7vC+huaTuPhInAhpzqpr4yU97KIjos2/83Itu/Cv8U1 13 | RAeEeRTkh0WjUfltoem/5f8bIdsCAwEAAaNTMFEwHQYDVR0OBBYEFHy+bEYqwvFU 14 | uQLTkIfQ36AM2DQiMB8GA1UdIwQYMBaAFHy+bEYqwvFUuQLTkIfQ36AM2DQiMA8G 15 | A1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQELBQADggEBAH3ANVzB59FcunZV/F8T 16 | RiCD6/gV7Jc3CswU8N8tVjzMCg2jOdTFF9iYZzNNKQvG13o/n5LkQr/lkKRQkWTx 17 | nkE5WZbR7vNqlzXgPa9NBiK5rPjgSt8azPW+Skct3Bj4B3PhTMSpoQ7PsUJ8UeV8 18 | kTNR5xrRLt6/mLfRJTXWXBM43GEZi8lL5q0nqz0tPGISADshHMo6ZlUu5Hvfp5v+ 19 | aonpO4sVS9hGOVxjGNMXYApEUy4jid9jjAfEk6jeELJMbXGLy/botFgIJK/QPe6P 20 | AfbdFgtg/qzG7Uy0A1iXXfWdgwmVrhCoGYYWCn4yWCAm894QKtdim87CHSDP0WUf 21 | Esg= 22 | -----END CERTIFICATE----- 23 | -------------------------------------------------------------------------------- /tests/magic_typing_test.py: -------------------------------------------------------------------------------- 1 | """Tests for acme.magic_typing.""" 2 | import sys 3 | import unittest 4 | 5 | import mock 6 | 7 | 8 | class MagicTypingTest(unittest.TestCase): 9 | """Tests for acme.magic_typing.""" 10 | def test_import_success(self): 11 | try: 12 | import typing as temp_typing 13 | except ImportError: # pragma: no cover 14 | temp_typing = None # pragma: no cover 15 | typing_class_mock = mock.MagicMock() 16 | text_mock = mock.MagicMock() 17 | typing_class_mock.Text = text_mock 18 | sys.modules['typing'] = typing_class_mock 19 | if 'acme.magic_typing' in sys.modules: 20 | del sys.modules['acme.magic_typing'] # pragma: no cover 21 | from acme.magic_typing import Text # pylint: disable=no-name-in-module 22 | self.assertEqual(Text, text_mock) 23 | del sys.modules['acme.magic_typing'] 24 | sys.modules['typing'] = temp_typing 25 | 26 | def test_import_failure(self): 27 | try: 28 | import typing as temp_typing 29 | except ImportError: # pragma: no cover 30 | temp_typing = None # pragma: no cover 31 | sys.modules['typing'] = None 32 | if 'acme.magic_typing' in sys.modules: 33 | del sys.modules['acme.magic_typing'] # pragma: no cover 34 | from acme.magic_typing import Text # pylint: disable=no-name-in-module 35 | self.assertTrue(Text is None) 36 | del sys.modules['acme.magic_typing'] 37 | sys.modules['typing'] = temp_typing 38 | 39 | 40 | if __name__ == '__main__': 41 | unittest.main() # pragma: no cover 42 | -------------------------------------------------------------------------------- /tests/errors_test.py: -------------------------------------------------------------------------------- 1 | """Tests for acme.errors.""" 2 | import unittest 3 | 4 | import mock 5 | 6 | 7 | class BadNonceTest(unittest.TestCase): 8 | """Tests for acme.errors.BadNonce.""" 9 | 10 | def setUp(self): 11 | from acme.errors import BadNonce 12 | self.error = BadNonce(nonce="xxx", error="error") 13 | 14 | def test_str(self): 15 | self.assertEqual("Invalid nonce ('xxx'): error", str(self.error)) 16 | 17 | 18 | class MissingNonceTest(unittest.TestCase): 19 | """Tests for acme.errors.MissingNonce.""" 20 | 21 | def setUp(self): 22 | from acme.errors import MissingNonce 23 | self.response = mock.MagicMock(headers={}) 24 | self.response.request.method = 'FOO' 25 | self.error = MissingNonce(self.response) 26 | 27 | def test_str(self): 28 | self.assertTrue("FOO" in str(self.error)) 29 | self.assertTrue("{}" in str(self.error)) 30 | 31 | 32 | class PollErrorTest(unittest.TestCase): 33 | """Tests for acme.errors.PollError.""" 34 | 35 | def setUp(self): 36 | from acme.errors import PollError 37 | self.timeout = PollError( 38 | exhausted=set([mock.sentinel.AR]), 39 | updated={}) 40 | self.invalid = PollError(exhausted=set(), updated={ 41 | mock.sentinel.AR: mock.sentinel.AR2}) 42 | 43 | def test_timeout(self): 44 | self.assertTrue(self.timeout.timeout) 45 | self.assertFalse(self.invalid.timeout) 46 | 47 | def test_repr(self): 48 | self.assertEqual('PollError(exhausted=%s, updated={sentinel.AR: ' 49 | 'sentinel.AR2})' % repr(set()), repr(self.invalid)) 50 | 51 | 52 | if __name__ == "__main__": 53 | unittest.main() # pragma: no cover 54 | -------------------------------------------------------------------------------- /tests/testdata/critical-san.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIErTCCA5WgAwIBAgIKETb7VQAAAAAdGTANBgkqhkiG9w0BAQsFADCBkTELMAkG 3 | A1UEBhMCVVMxDTALBgNVBAgTBFV0YWgxFzAVBgNVBAcTDlNhbHQgTGFrZSBDaXR5 4 | MRUwEwYDVQQKEwxWZW5hZmksIEluYy4xHzAdBgNVBAsTFkRlbW9uc3RyYXRpb24g 5 | U2VydmljZXMxIjAgBgNVBAMTGVZlbmFmaSBFeGFtcGxlIElzc3VpbmcgQ0EwHhcN 6 | MTcwNzEwMjMxNjA1WhcNMTcwODA5MjMxNjA1WjAAMIIBIjANBgkqhkiG9w0BAQEF 7 | AAOCAQ8AMIIBCgKCAQEA7CU5qRIzCs9hCRiSUvLZ8r81l4zIYbx1V1vZz6x1cS4M 8 | 0keNfFJ1wB+zuvx80KaMYkWPYlg4Rsm9Ok3ZapakXDlaWtrfg78lxtHuPw1o7AYV 9 | EXDwwPkNugLMJfYw5hWYSr8PCLcOJoY00YQ0fJ44L+kVsUyGjN4UTRRZmOh/yNVU 10 | 0W12dTCz4X7BAW01OuY6SxxwewnW3sBEep+APfr2jd/oIx7fgZmVB8aRCDPj4AFl 11 | XINWIwxmptOwnKPbwLN/vhCvJRUkO6rA8lpYwQkedFf6fHhqi2Sq/NCEOg4RvMCF 12 | fKbMpncOXxz+f4/i43SVLrPz/UyhjNbKGJZ+zFrQowIDAQABo4IBlTCCAZEwPgYD 13 | VR0RAQH/BDQwMoIbY2hpY2Fnby1jdWJzLnZlbmFmaS5leGFtcGxlghNjdWJzLnZl 14 | bmFmaS5leGFtcGxlMB0GA1UdDgQWBBTgKZXVSFNyPHHtO/phtIALPcCF5DAfBgNV 15 | HSMEGDAWgBT/JJ6Wei/pzf+9DRHuv6Wgdk2HsjBSBgNVHR8ESzBJMEegRaBDhkFo 16 | dHRwOi8vcGtpLnZlbmFmaS5leGFtcGxlL2NybC9WZW5hZmklMjBFeGFtcGxlJTIw 17 | SXNzdWluZyUyMENBLmNybDA6BggrBgEFBQcBAQQuMCwwKgYIKwYBBQUHMAGGHmh0 18 | dHA6Ly9wa2kudmVuYWZpLmV4YW1wbGUvb2NzcDAOBgNVHQ8BAf8EBAMCBaAwPQYJ 19 | KwYBBAGCNxUHBDAwLgYmKwYBBAGCNxUIhIDLGYTvsSSEnZ8ehvD5UofP4hMEgobv 20 | DIGy4mcCAWQCAQIwEwYDVR0lBAwwCgYIKwYBBQUHAwEwGwYJKwYBBAGCNxUKBA4w 21 | DDAKBggrBgEFBQcDATANBgkqhkiG9w0BAQsFAAOCAQEA3YW4t1AzxEn384OqdU6L 22 | ny8XkMhWpRM0W0Z9ZC3gRZKbVUu49nG/KB5hbVn/de33zdX9HOZJKc0vXzkGZQUs 23 | OUCCsKX4VKzV5naGXOuGRbvV4CJh5P0kPlDzyb5t312S49nJdcdBf0Y/uL5Qzhst 24 | bXy8qNfFNG3SIKKRAUpqE9OVIl+F+JBwexa+v/4dFtUOqMipfXxB3TaxnDqvU1dS 25 | yO34ZTvIMGXJIZ5nn/d/LNc3N3vBg2SHkMpladqw0Hr7mL0bFOe0b+lJgkDP06Be 26 | n08fikhz1j2AW4/ZHa9w4DUz7J21+RtHMhh+Vd1On0EAeZ563svDe7Z+yrg6zOVv 27 | KA== 28 | -----END CERTIFICATE----- -------------------------------------------------------------------------------- /tests/testdata/csr-idnsans.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE REQUEST----- 2 | MIIEpzCCBFECAQAwZDELMAkGA1UECAwCQ0ExFjAUBgNVBAcMDVNhbiBGcmFuY2lz 3 | Y28xJzAlBgNVBAsMHkVsZWN0cm9uaWMgRnJvbnRpZXIgRm91bmRhdGlvbjEUMBIG 4 | A1UEAwwLZXhhbXBsZS5jb20wXDANBgkqhkiG9w0BAQEFAANLADBIAkEArHVztFHt 5 | H92ucFJD/N/HW9AsdRsUuHUBBBDlHwNlRd3fp580rv2+6QWE30cWgdmJS86ObRz6 6 | lUTor4R0T+3C5QIDAQABoIIDhjCCA4IGCSqGSIb3DQEJDjGCA3MwggNvMAkGA1Ud 7 | EwQCMAAwCwYDVR0PBAQDAgXgMIIDUwYDVR0RBIIDSjCCA0aCYs+Dz4TPhc+Gz4fP 8 | iM+Jz4rPi8+Mz43Pjs+Pz5DPkc+Sz5PPlM+Vz5bPl8+Yz5nPms+bz5zPnc+ez5/P 9 | oM+hz6LPo8+kz6XPps+nz6jPqc+qz6vPrM+tz67Pry5pbnZhbGlkgmLPsM+xz7LP 10 | s8+0z7XPts+3z7jPuc+6z7vPvM+9z77Pv9mB2YLZg9mE2YXZhtmH2YjZidmK2YvZ 11 | jNmN2Y7Zj9mQ2ZHZktmT2ZTZldmW2ZfZmNmZ2ZrZm9mc2Z0uaW52YWxpZIJi2Z7Z 12 | n9mg2aHZotmj2aTZpdmm2afZqNmp2arZq9ms2a3Zrtmv2bDZsdmy2bPZtNm12bbZ 13 | t9m42bnZutm72bzZvdm+2b/agNqB2oLag9qE2oXahtqH2ojaidqKLmludmFsaWSC 14 | YtqL2ozajdqO2o/akNqR2pLak9qU2pXaltqX2pjamdqa2pvanNqd2p7an9qg2qHa 15 | otqj2qTapdqm2qfaqNqp2qraq9qs2q3artqv2rDasdqy2rPatNq12rbaty5pbnZh 16 | bGlkgmLauNq52rrau9q82r3avtq/24DbgduC24PbhNuF24bbh9uI24nbituL24zb 17 | jduO24/bkNuR25Lbk9uU25XbltuX25jbmdua25vbnNud257bn9ug26Hbotuj26Qu 18 | aW52YWxpZIJ426Xbptun26jbqduq26vbrNut267br9uw27Hbstuz27Tbtdu227fb 19 | uNu527rbu+GgoOGgoeGgouGgo+GgpOGgpeGgpuGgp+GgqOGgqeGgquGgq+GgrOGg 20 | reGgruGgr+GgsOGgseGgsuGgs+GgtOGgtS5pbnZhbGlkgoGP4aC24aC34aC44aC5 21 | 4aC64aC74aC84aC94aC+4aC/4aGA4aGB4aGC4aGD4aGE4aGF4aGG4aGH4aGI4aGJ 22 | 4aGK4aGL4aGM4aGN4aGO4aGP4aGQ4aGR4aGS4aGT4aGU4aGV4aGW4aGX4aGY4aGZ 23 | 4aGa4aGb4aGc4aGd4aGe4aGf4aGg4aGh4aGiLmludmFsaWSCROGho+GhpOGhpeGh 24 | puGhp+GhqOGhqeGhquGhq+GhrOGhreGhruGhr+GhsOGhseGhsuGhs+GhtOGhteGh 25 | ti5pbnZhbGlkMA0GCSqGSIb3DQEBCwUAA0EAeNkY0M0+kMnjRo6dEUoGE4dX9fEr 26 | dfGrpPUBcwG0P5QBdZJWvZxTfRl14yuPYHbGHULXeGqRdkU6HK5pOlzpng== 27 | -----END CERTIFICATE REQUEST----- 28 | -------------------------------------------------------------------------------- /tests/testdata/rsa2048_key.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN PRIVATE KEY----- 2 | MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQDaFU9aXb0VFATq 3 | r5uzNnmyxB1e4YoFB+6gBlGRJHz+luGK4mFWPiB6x9bLzT0mvqD0yu8HgzZk9dXo 4 | VpxshAiDPc9q9h4vclVdMnMYeIVkODbfAQCmcAsGl6clpuxo+pthM6UjeQ3eu4OW 5 | AEAa1FIdfkUWxYvXNC7P+mThMTL34PJKreHNrFwoB9SBZ8b7zowWKKe8YdMdy9QJ 6 | MmsdLcPKoEKf8+ZpLqZlQzSWDxZm8xTzDcg0etXnekFEw4Oz+FALhZHoyy8xb8+W 7 | Gju7wvobmk7j4SJwIac6qa+MlPeyiI6LNv/NyLbvwr/FNUQHhHkU5IdFo1H5baHp 8 | v+X/GyHbAgMBAAECggEAURFe4C68XRuGAF+rN2Fmt+djK6QXlGswb1gp9hRkSpd3 9 | 3BLvMAoENOAYnsX6l26Bkr3lQRurmrgv/iBEIaqrJ25QrmgzLFwKE4zvcAdNPsYO 10 | z7MltLktwBOb1MlKVHPkUqvKFXeoikWWUqphKhgHNmN7900UALmrNTDVU0jgs3fB 11 | o35o8d5SjoC52K4wCTjhPyjt4cdbfbziRs2qFhfGdawidRO1xLlDM4tTTW+5yWGK 12 | lt0SwyvDVC6XWeNoT3nXyKjXWP7hcYqm0iS7ffL9YzEC2RXNGQUqeR50i9Y0rDdH 13 | Vqcr+Rqio2ww68zbDWBpC/jU133BSoHuSE1wstxIkQKBgQDxlEr42WJfgdajbZ1a 14 | hUIeLEgvhezLmD1hcYwZuQCLgizmY2ovvmeAH74koCDEsUUQunPYHsRla7wT3q1/ 15 | IkR1KgJPwESpkQaKuAqxeEAkv7Gn8Lzcn22jCoRCfGA68wKJz2ECFZDc0RDvRrT/ 16 | 9GhiiGUoO47jv9ezrSDO1eu5/QKBgQDnGfYVMNLiA0fy4AxSyY2vdo7vruOFGpRP 17 | n94gwxZ+0dQDWHzn3J4rHivxtcyd/MOZv4I8PtYK7tmmjYv1ngQ6sGl4p8bpUtwj 18 | 9++/B1CyB1W5/VPqMkd+Sj0dbejycME55+F6/r4basPXxBFFCfknjAlVvyvbBhUy 19 | ftNpHxZGtwKBgChJM4t2LPqCW3nbgL8ks9b2SX9rVQbKt4m1dsifWmDpb3VoJMAb 20 | f4UVRg8ziONkMIFOppzm3JeRNMcXflVSMJpdTA9in9CrN60QbfAUfpXiRc0cz1H3 21 | YEAtM8smlKGf/s9efu3rDMJWNv3AC9UXPAUae8wOypBeYKk8+NilQe89AoGAXEA3 22 | xFO+CqyGnwQixzVf0qf//NuSRQLMK1DEyc02gJ9gA4niKmgd11Zu8kjBClvo9MnG 23 | wifPJ4Qa6+pa8UwHoinjoF9Q/rit2cnSMS5JXxegd+MRCU7SzS3zYXkLYSPzbhsL 24 | Hh7sYmNnFA1XW3jUtZ2n6EusxPyTn5mS6MaZDNcCgYBelFKFjNIQ50NbOnm8DewK 25 | jUd5OFKowKodlQVcHiF9CVbjvpN8ZPRcBSmqDU4kpT/rmcybVoL6Zfa/zWkw8+Oh 26 | QxKb3BYf5vRUMd/RA+/t5KG4ZOIIYB3qoltAYlhVaINukL6cGVG1qvV/ntcsfsn6 27 | kmf1UgGFcKrJuXgwEtTVxw== 28 | -----END PRIVATE KEY----- 29 | -------------------------------------------------------------------------------- /acme.egg-info/SOURCES.txt: -------------------------------------------------------------------------------- 1 | LICENSE.txt 2 | MANIFEST.in 3 | README.rst 4 | pytest.ini 5 | setup.cfg 6 | setup.py 7 | acme/__init__.py 8 | acme/challenges.py 9 | acme/client.py 10 | acme/crypto_util.py 11 | acme/errors.py 12 | acme/fields.py 13 | acme/jws.py 14 | acme/magic_typing.py 15 | acme/messages.py 16 | acme/standalone.py 17 | acme/util.py 18 | acme.egg-info/PKG-INFO 19 | acme.egg-info/SOURCES.txt 20 | acme.egg-info/dependency_links.txt 21 | acme.egg-info/requires.txt 22 | acme.egg-info/top_level.txt 23 | docs/.gitignore 24 | docs/Makefile 25 | docs/api.rst 26 | docs/conf.py 27 | docs/index.rst 28 | docs/jws-help.txt 29 | docs/make.bat 30 | docs/_static/.gitignore 31 | docs/_templates/.gitignore 32 | docs/api/challenges.rst 33 | docs/api/client.rst 34 | docs/api/errors.rst 35 | docs/api/fields.rst 36 | docs/api/jose.rst 37 | docs/api/messages.rst 38 | docs/api/standalone.rst 39 | docs/man/jws.rst 40 | examples/http01_example.py 41 | examples/standalone/README 42 | tests/challenges_test.py 43 | tests/client_test.py 44 | tests/crypto_util_test.py 45 | tests/errors_test.py 46 | tests/fields_test.py 47 | tests/jose_test.py 48 | tests/jws_test.py 49 | tests/magic_typing_test.py 50 | tests/messages_test.py 51 | tests/standalone_test.py 52 | tests/test_util.py 53 | tests/util_test.py 54 | tests/testdata/README 55 | tests/testdata/cert-100sans.pem 56 | tests/testdata/cert-idnsans.pem 57 | tests/testdata/cert-nocn.der 58 | tests/testdata/cert-san.pem 59 | tests/testdata/cert.der 60 | tests/testdata/cert.pem 61 | tests/testdata/critical-san.pem 62 | tests/testdata/csr-100sans.pem 63 | tests/testdata/csr-6sans.pem 64 | tests/testdata/csr-idnsans.pem 65 | tests/testdata/csr-nosans.pem 66 | tests/testdata/csr-san.pem 67 | tests/testdata/csr.der 68 | tests/testdata/csr.pem 69 | tests/testdata/dsa512_key.pem 70 | tests/testdata/rsa1024_key.pem 71 | tests/testdata/rsa2048_cert.pem 72 | tests/testdata/rsa2048_key.pem 73 | tests/testdata/rsa256_key.pem 74 | tests/testdata/rsa512_key.pem -------------------------------------------------------------------------------- /tests/testdata/cert-idnsans.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIFNjCCBOCgAwIBAgIJAP4rNqqOKifCMA0GCSqGSIb3DQEBCwUAMGQxCzAJBgNV 3 | BAgMAkNBMRYwFAYDVQQHDA1TYW4gRnJhbmNpc2NvMScwJQYDVQQLDB5FbGVjdHJv 4 | bmljIEZyb250aWVyIEZvdW5kYXRpb24xFDASBgNVBAMMC2V4YW1wbGUuY29tMB4X 5 | DTE2MDEwNjIwMDg1OFoXDTE2MDEwNzIwMDg1OFowZDELMAkGA1UECAwCQ0ExFjAU 6 | BgNVBAcMDVNhbiBGcmFuY2lzY28xJzAlBgNVBAsMHkVsZWN0cm9uaWMgRnJvbnRp 7 | ZXIgRm91bmRhdGlvbjEUMBIGA1UEAwwLZXhhbXBsZS5jb20wXDANBgkqhkiG9w0B 8 | AQEFAANLADBIAkEArHVztFHtH92ucFJD/N/HW9AsdRsUuHUBBBDlHwNlRd3fp580 9 | rv2+6QWE30cWgdmJS86ObRz6lUTor4R0T+3C5QIDAQABo4IDczCCA28wCQYDVR0T 10 | BAIwADALBgNVHQ8EBAMCBeAwggNTBgNVHREEggNKMIIDRoJiz4PPhM+Fz4bPh8+I 11 | z4nPis+Lz4zPjc+Oz4/PkM+Rz5LPk8+Uz5XPls+Xz5jPmc+az5vPnM+dz57Pn8+g 12 | z6HPos+jz6TPpc+mz6fPqM+pz6rPq8+sz63Prs+vLmludmFsaWSCYs+wz7HPss+z 13 | z7TPtc+2z7fPuM+5z7rPu8+8z73Pvs+/2YHZgtmD2YTZhdmG2YfZiNmJ2YrZi9mM 14 | 2Y3ZjtmP2ZDZkdmS2ZPZlNmV2ZbZl9mY2ZnZmtmb2ZzZnS5pbnZhbGlkgmLZntmf 15 | 2aDZodmi2aPZpNml2abZp9mo2anZqtmr2azZrdmu2a/ZsNmx2bLZs9m02bXZttm3 16 | 2bjZudm62bvZvNm92b7Zv9qA2oHagtqD2oTahdqG2ofaiNqJ2oouaW52YWxpZIJi 17 | 2ovajNqN2o7aj9qQ2pHaktqT2pTaldqW2pfamNqZ2pram9qc2p3antqf2qDaodqi 18 | 2qPapNql2qbap9qo2qnaqtqr2qzardqu2q/asNqx2rLas9q02rXattq3LmludmFs 19 | aWSCYtq42rnautq72rzavdq+2r/bgNuB24Lbg9uE24XbhtuH24jbiduK24vbjNuN 20 | 247bj9uQ25HbktuT25TblduW25fbmNuZ25rbm9uc253bntuf26Dbodui26PbpC5p 21 | bnZhbGlkgnjbpdum26fbqNup26rbq9us263brtuv27Dbsduy27PbtNu127bbt9u4 22 | 27nbutu74aCg4aCh4aCi4aCj4aCk4aCl4aCm4aCn4aCo4aCp4aCq4aCr4aCs4aCt 23 | 4aCu4aCv4aCw4aCx4aCy4aCz4aC04aC1LmludmFsaWSCgY/hoLbhoLfhoLjhoLnh 24 | oLrhoLvhoLzhoL3hoL7hoL/hoYDhoYHhoYLhoYPhoYThoYXhoYbhoYfhoYjhoYnh 25 | oYrhoYvhoYzhoY3hoY7hoY/hoZDhoZHhoZLhoZPhoZThoZXhoZbhoZfhoZjhoZnh 26 | oZrhoZvhoZzhoZ3hoZ7hoZ/hoaDhoaHhoaIuaW52YWxpZIJE4aGj4aGk4aGl4aGm 27 | 4aGn4aGo4aGp4aGq4aGr4aGs4aGt4aGu4aGv4aGw4aGx4aGy4aGz4aG04aG14aG2 28 | LmludmFsaWQwDQYJKoZIhvcNAQELBQADQQAzOQL/54yXxln87/YvEQbBm9ik9zoT 29 | TxEkvnZ4kmTRhDsUPtRjMXhY2FH7LOtXKnJQ7POUB7AsJ2Z6uq2w623G 30 | -----END CERTIFICATE----- 31 | -------------------------------------------------------------------------------- /acme/fields.py: -------------------------------------------------------------------------------- 1 | """ACME JSON fields.""" 2 | import logging 3 | 4 | import josepy as jose 5 | import pyrfc3339 6 | 7 | logger = logging.getLogger(__name__) 8 | 9 | 10 | class Fixed(jose.Field): 11 | """Fixed field.""" 12 | 13 | def __init__(self, json_name, value): 14 | self.value = value 15 | super(Fixed, self).__init__( 16 | json_name=json_name, default=value, omitempty=False) 17 | 18 | def decode(self, value): 19 | if value != self.value: 20 | raise jose.DeserializationError('Expected {0!r}'.format(self.value)) 21 | return self.value 22 | 23 | def encode(self, value): 24 | if value != self.value: 25 | logger.warning( 26 | 'Overriding fixed field (%s) with %r', self.json_name, value) 27 | return value 28 | 29 | 30 | class RFC3339Field(jose.Field): 31 | """RFC3339 field encoder/decoder. 32 | 33 | Handles decoding/encoding between RFC3339 strings and aware (not 34 | naive) `datetime.datetime` objects 35 | (e.g. ``datetime.datetime.now(pytz.utc)``). 36 | 37 | """ 38 | 39 | @classmethod 40 | def default_encoder(cls, value): 41 | return pyrfc3339.generate(value) 42 | 43 | @classmethod 44 | def default_decoder(cls, value): 45 | try: 46 | return pyrfc3339.parse(value) 47 | except ValueError as error: 48 | raise jose.DeserializationError(error) 49 | 50 | 51 | class Resource(jose.Field): 52 | """Resource MITM field.""" 53 | 54 | def __init__(self, resource_type, *args, **kwargs): 55 | self.resource_type = resource_type 56 | super(Resource, self).__init__( 57 | 'resource', default=resource_type, *args, **kwargs) 58 | 59 | def decode(self, value): 60 | if value != self.resource_type: 61 | raise jose.DeserializationError( 62 | 'Wrong resource type: {0} instead of {1}'.format( 63 | value, self.resource_type)) 64 | return value 65 | -------------------------------------------------------------------------------- /tests/jose_test.py: -------------------------------------------------------------------------------- 1 | """Tests for acme.jose shim.""" 2 | import importlib 3 | import unittest 4 | 5 | 6 | class JoseTest(unittest.TestCase): 7 | """Tests for acme.jose shim.""" 8 | 9 | def _test_it(self, submodule, attribute): 10 | if submodule: 11 | acme_jose_path = 'acme.jose.' + submodule 12 | josepy_path = 'josepy.' + submodule 13 | else: 14 | acme_jose_path = 'acme.jose' 15 | josepy_path = 'josepy' 16 | acme_jose_mod = importlib.import_module(acme_jose_path) 17 | josepy_mod = importlib.import_module(josepy_path) 18 | 19 | self.assertIs(acme_jose_mod, josepy_mod) 20 | self.assertIs(getattr(acme_jose_mod, attribute), getattr(josepy_mod, attribute)) 21 | 22 | # We use the imports below with eval, but pylint doesn't 23 | # understand that. 24 | import acme # pylint: disable=unused-import 25 | import josepy # pylint: disable=unused-import 26 | acme_jose_mod = eval(acme_jose_path) # pylint: disable=eval-used 27 | josepy_mod = eval(josepy_path) # pylint: disable=eval-used 28 | self.assertIs(acme_jose_mod, josepy_mod) 29 | self.assertIs(getattr(acme_jose_mod, attribute), getattr(josepy_mod, attribute)) 30 | 31 | def test_top_level(self): 32 | self._test_it('', 'RS512') 33 | 34 | def test_submodules(self): 35 | # This test ensures that the modules in josepy that were 36 | # available at the time it was moved into its own package are 37 | # available under acme.jose. Backwards compatibility with new 38 | # modules or testing code is not maintained. 39 | mods_and_attrs = [('b64', 'b64decode',), 40 | ('errors', 'Error',), 41 | ('interfaces', 'JSONDeSerializable',), 42 | ('json_util', 'Field',), 43 | ('jwa', 'HS256',), 44 | ('jwk', 'JWK',), 45 | ('jws', 'JWS',), 46 | ('util', 'ImmutableMap',),] 47 | 48 | for mod, attr in mods_and_attrs: 49 | self._test_it(mod, attr) 50 | 51 | 52 | if __name__ == '__main__': 53 | unittest.main() # pragma: no cover 54 | -------------------------------------------------------------------------------- /acme/jws.py: -------------------------------------------------------------------------------- 1 | """ACME-specific JWS. 2 | 3 | The JWS implementation in josepy only implements the base JOSE standard. In 4 | order to support the new header fields defined in ACME, this module defines some 5 | ACME-specific classes that layer on top of josepy. 6 | """ 7 | import josepy as jose 8 | 9 | 10 | class Header(jose.Header): 11 | """ACME-specific JOSE Header. Implements nonce, kid, and url. 12 | """ 13 | nonce = jose.Field('nonce', omitempty=True, encoder=jose.encode_b64jose) 14 | kid = jose.Field('kid', omitempty=True) 15 | url = jose.Field('url', omitempty=True) 16 | 17 | @nonce.decoder 18 | def nonce(value): # pylint: disable=missing-docstring,no-self-argument 19 | try: 20 | return jose.decode_b64jose(value) 21 | except jose.DeserializationError as error: 22 | # TODO: custom error 23 | raise jose.DeserializationError("Invalid nonce: {0}".format(error)) 24 | 25 | 26 | class Signature(jose.Signature): 27 | """ACME-specific Signature. Uses ACME-specific Header for customer fields.""" 28 | __slots__ = jose.Signature._orig_slots # pylint: disable=no-member 29 | 30 | # TODO: decoder/encoder should accept cls? Otherwise, subclassing 31 | # JSONObjectWithFields is tricky... 32 | header_cls = Header 33 | header = jose.Field( 34 | 'header', omitempty=True, default=header_cls(), 35 | decoder=header_cls.from_json) 36 | 37 | # TODO: decoder should check that nonce is in the protected header 38 | 39 | 40 | class JWS(jose.JWS): 41 | """ACME-specific JWS. Includes none, url, and kid in protected header.""" 42 | signature_cls = Signature 43 | __slots__ = jose.JWS._orig_slots 44 | 45 | @classmethod 46 | # pylint: disable=arguments-differ 47 | def sign(cls, payload, key, alg, nonce, url=None, kid=None): 48 | # Per ACME spec, jwk and kid are mutually exclusive, so only include a 49 | # jwk field if kid is not provided. 50 | include_jwk = kid is None 51 | return super(JWS, cls).sign(payload, key=key, alg=alg, 52 | protect=frozenset(['nonce', 'url', 'kid', 'jwk', 'alg']), 53 | nonce=nonce, url=url, kid=kid, 54 | include_jwk=include_jwk) 55 | -------------------------------------------------------------------------------- /tests/test_util.py: -------------------------------------------------------------------------------- 1 | """Test utilities. 2 | 3 | .. warning:: This module is not part of the public API. 4 | 5 | """ 6 | import os 7 | 8 | from cryptography.hazmat.backends import default_backend 9 | from cryptography.hazmat.primitives import serialization 10 | import josepy as jose 11 | from OpenSSL import crypto 12 | import pkg_resources 13 | 14 | 15 | def load_vector(*names): 16 | """Load contents of a test vector.""" 17 | # luckily, resource_string opens file in binary mode 18 | return pkg_resources.resource_string( 19 | __name__, os.path.join('testdata', *names)) 20 | 21 | 22 | def _guess_loader(filename, loader_pem, loader_der): 23 | _, ext = os.path.splitext(filename) 24 | if ext.lower() == '.pem': 25 | return loader_pem 26 | elif ext.lower() == '.der': 27 | return loader_der 28 | raise ValueError("Loader could not be recognized based on extension") # pragma: no cover 29 | 30 | 31 | def load_cert(*names): 32 | """Load certificate.""" 33 | loader = _guess_loader( 34 | names[-1], crypto.FILETYPE_PEM, crypto.FILETYPE_ASN1) 35 | return crypto.load_certificate(loader, load_vector(*names)) 36 | 37 | 38 | def load_comparable_cert(*names): 39 | """Load ComparableX509 cert.""" 40 | return jose.ComparableX509(load_cert(*names)) 41 | 42 | 43 | def load_csr(*names): 44 | """Load certificate request.""" 45 | loader = _guess_loader( 46 | names[-1], crypto.FILETYPE_PEM, crypto.FILETYPE_ASN1) 47 | return crypto.load_certificate_request(loader, load_vector(*names)) 48 | 49 | 50 | def load_comparable_csr(*names): 51 | """Load ComparableX509 certificate request.""" 52 | return jose.ComparableX509(load_csr(*names)) 53 | 54 | 55 | def load_rsa_private_key(*names): 56 | """Load RSA private key.""" 57 | loader = _guess_loader(names[-1], serialization.load_pem_private_key, 58 | serialization.load_der_private_key) 59 | return jose.ComparableRSAKey(loader( 60 | load_vector(*names), password=None, backend=default_backend())) 61 | 62 | 63 | def load_pyopenssl_private_key(*names): 64 | """Load pyOpenSSL private key.""" 65 | loader = _guess_loader( 66 | names[-1], crypto.FILETYPE_PEM, crypto.FILETYPE_ASN1) 67 | return crypto.load_privatekey(loader, load_vector(*names)) 68 | -------------------------------------------------------------------------------- /tests/jws_test.py: -------------------------------------------------------------------------------- 1 | """Tests for acme.jws.""" 2 | import unittest 3 | 4 | import josepy as jose 5 | 6 | import test_util 7 | 8 | KEY = jose.JWKRSA.load(test_util.load_vector('rsa512_key.pem')) 9 | 10 | 11 | class HeaderTest(unittest.TestCase): 12 | """Tests for acme.jws.Header.""" 13 | 14 | good_nonce = jose.encode_b64jose(b'foo') 15 | wrong_nonce = u'F' 16 | # Following just makes sure wrong_nonce is wrong 17 | try: 18 | jose.b64decode(wrong_nonce) 19 | except (ValueError, TypeError): 20 | assert True 21 | else: 22 | assert False # pragma: no cover 23 | 24 | def test_nonce_decoder(self): 25 | from acme.jws import Header 26 | nonce_field = Header._fields['nonce'] 27 | 28 | self.assertRaises( 29 | jose.DeserializationError, nonce_field.decode, self.wrong_nonce) 30 | self.assertEqual(b'foo', nonce_field.decode(self.good_nonce)) 31 | 32 | 33 | class JWSTest(unittest.TestCase): 34 | """Tests for acme.jws.JWS.""" 35 | 36 | def setUp(self): 37 | self.privkey = KEY 38 | self.pubkey = self.privkey.public_key() 39 | self.nonce = jose.b64encode(b'Nonce') 40 | self.url = 'hi' 41 | self.kid = 'baaaaa' 42 | 43 | def test_kid_serialize(self): 44 | from acme.jws import JWS 45 | jws = JWS.sign(payload=b'foo', key=self.privkey, 46 | alg=jose.RS256, nonce=self.nonce, 47 | url=self.url, kid=self.kid) 48 | self.assertEqual(jws.signature.combined.nonce, self.nonce) 49 | self.assertEqual(jws.signature.combined.url, self.url) 50 | self.assertEqual(jws.signature.combined.kid, self.kid) 51 | self.assertEqual(jws.signature.combined.jwk, None) 52 | # TODO: check that nonce is in protected header 53 | 54 | self.assertEqual(jws, JWS.from_json(jws.to_json())) 55 | 56 | def test_jwk_serialize(self): 57 | from acme.jws import JWS 58 | jws = JWS.sign(payload=b'foo', key=self.privkey, 59 | alg=jose.RS256, nonce=self.nonce, 60 | url=self.url) 61 | self.assertEqual(jws.signature.combined.kid, None) 62 | self.assertEqual(jws.signature.combined.jwk, self.pubkey) 63 | 64 | 65 | if __name__ == '__main__': 66 | unittest.main() # pragma: no cover 67 | -------------------------------------------------------------------------------- /tests/fields_test.py: -------------------------------------------------------------------------------- 1 | """Tests for acme.fields.""" 2 | import datetime 3 | import unittest 4 | 5 | import josepy as jose 6 | import pytz 7 | 8 | 9 | class FixedTest(unittest.TestCase): 10 | """Tests for acme.fields.Fixed.""" 11 | 12 | def setUp(self): 13 | from acme.fields import Fixed 14 | self.field = Fixed('name', 'x') 15 | 16 | def test_decode(self): 17 | self.assertEqual('x', self.field.decode('x')) 18 | 19 | def test_decode_bad(self): 20 | self.assertRaises(jose.DeserializationError, self.field.decode, 'y') 21 | 22 | def test_encode(self): 23 | self.assertEqual('x', self.field.encode('x')) 24 | 25 | def test_encode_override(self): 26 | self.assertEqual('y', self.field.encode('y')) 27 | 28 | 29 | class RFC3339FieldTest(unittest.TestCase): 30 | """Tests for acme.fields.RFC3339Field.""" 31 | 32 | def setUp(self): 33 | self.decoded = datetime.datetime(2015, 3, 27, tzinfo=pytz.utc) 34 | self.encoded = '2015-03-27T00:00:00Z' 35 | 36 | def test_default_encoder(self): 37 | from acme.fields import RFC3339Field 38 | self.assertEqual( 39 | self.encoded, RFC3339Field.default_encoder(self.decoded)) 40 | 41 | def test_default_encoder_naive_fails(self): 42 | from acme.fields import RFC3339Field 43 | self.assertRaises( 44 | ValueError, RFC3339Field.default_encoder, datetime.datetime.now()) 45 | 46 | def test_default_decoder(self): 47 | from acme.fields import RFC3339Field 48 | self.assertEqual( 49 | self.decoded, RFC3339Field.default_decoder(self.encoded)) 50 | 51 | def test_default_decoder_raises_deserialization_error(self): 52 | from acme.fields import RFC3339Field 53 | self.assertRaises( 54 | jose.DeserializationError, RFC3339Field.default_decoder, '') 55 | 56 | 57 | class ResourceTest(unittest.TestCase): 58 | """Tests for acme.fields.Resource.""" 59 | 60 | def setUp(self): 61 | from acme.fields import Resource 62 | self.field = Resource('x') 63 | 64 | def test_decode_good(self): 65 | self.assertEqual('x', self.field.decode('x')) 66 | 67 | def test_decode_wrong(self): 68 | self.assertRaises(jose.DeserializationError, self.field.decode, 'y') 69 | 70 | 71 | if __name__ == '__main__': 72 | unittest.main() # pragma: no cover 73 | -------------------------------------------------------------------------------- /debian/upstream/signing-key.asc: -------------------------------------------------------------------------------- 1 | -----BEGIN PGP PUBLIC KEY BLOCK----- 2 | 3 | mQENBFZVq4kBCADJvp9fLg1WqQ3KJl9ayOk23i5PNGSF6loT2muvoUcbQFUKC6ie 4 | xC3chvIIIrXPG1lJhNxXONUaiooBrDLo17MGM5C6k8j5FZfAqxirC40rL4yDF+cq 5 | 2ObuURaWX6t0eS9k6B0Kg8aqru9bKHO/NQNqN/nw8Kyyg5D2jdn2HPcMn6/5RWrv 6 | q2TRk3lFggunm4wb2i8Gegu04/bgcfEyxvI0Y+gLR4n3vu1/m4oEVuwxwqggb5BB 7 | Ac5knkiCNZl6sGwZxCXxJcK4J+3O5RNdF7K7v/B8S8djN6fKmcjtPn0tsB6xkaQ7 8 | osaGQy2dOlh3ZWZDhtACCBJmCp1hx5zerkuJABEBAAG0NkxldCdzIEVuY3J5cHQg 9 | Q2xpZW50IFRlYW0gPGxldHNlbmNyeXB0LWNsaWVudEBlZmYub3JnPokBPgQTAQIA 10 | KAUCVlWriQIbAwUJFo5qAAYLCQgHAwIGFQgCCQoLBBYCAwECHgECF4AACgkQTRfJ 11 | lc2XdfISLAf/XJzoA/L2cIErpnJUuhuPcHHDnt2wspiNuzpwH5ojob6L3DTXCYrE 12 | NQUm8wbBgruDpO9OLvQrzmlRIdDU/IkcHD6lfiRT3lhfAcZBZfEVqhSvyRP2VABX 13 | 1V+bSGJM9sLJZqgw26fD7GX5VUxvZfeqN4wW4ufdtrkRb+evtuOWDGjV/OfD285U 14 | 0mv1JIdJ/DnUXoNDn1Lr7RJJtTfYUzQXAvgmB2Fkn1nkg3drhJ8+mj5VAzRTEX8a 15 | zb/ss56cW8BFGNWcfwefMQq5PLQIOczBtkVyTNKKwMorwfTcp0GnNSNil/mTkrcJ 16 | mjRMTbXwlNxcq+G7Sg7hG6+PYj2zbjXaD7kBDQRWVauJAQgAwXk3jChBJmlH7ir4 17 | IPVC8D8FI3oqMotEX05DbvjZB0+S8MCqkxor5MaMBEXZMiMUO7u5+FRWwFL1befI 18 | PFxKI48PRm1hZNaQPu+3qXfEutCWhNYBIQogSdN8oOg2HX+tNk9OUryRhIdeDhYE 19 | PtZzJv5bca9GaJilhMJrKuK4FdQFiCQVXLKwY7g2knzIG81IyQj+pd0EhJlMeGU4 20 | WVXA/LG4tOejRCkJSNAEeFktNOYKR3ERWwgZxHB8/apPeww80Kk6Pbc9uPfGTeec 21 | pcpwdUqIxTzkfkdb6SL7VQa01BzgbidFeKEKCPD7eq/kATcUPl6q+fC9AismlKmC 22 | zU/a4QARAQABiQElBBgBAgAPBQJWVauJAhsMBQkWjmoAAAoJEE0XyZXNl3Xyoz4I 23 | AJ8HVTvss13crU2SBNIFce2EIkXquUPqnv6vuNFFq+3Qv4atHch+p3rnkSZ8yTud 24 | IT0tyYO/5dRPoiKFzh2HqHftKe61oT1i6xGkfQmMdz2Y1A1Jl6EUEs8/8uiDONtz 25 | 7PrKTMcIQOSRdUkDHO8OXALiA/it20cVLq39bP7bFDT31bIGyRKlF9beNnd1BINT 26 | QPa1O8JxeE6NLPdmGeHAXyEPUgcjvXrCLKUSvM0KlB81N0SjX0RpM7qyX3XLnj+f 27 | QOJ+0pbvluMnn2Ooejkz9F6bNr1SN9cu0TWFMgoqvES0mL3PD6dSW5QNfIDNy+TA 28 | zaOjYTu55/3JvbyRD26ouau5AQ0EVlWsMwEIALhDTFjI97adohYQMgIBFbfkY1ET 29 | btQiwyxqBMOVDY5857cYgY5KKcdM50Y9SbK0VX9ScBsB0x28IIr/gBHk5SB0yc7L 30 | xVByT3oOf6dktXLS2LljIFwsz+g1qi7bdS3ROBmQW8U1Jbae/XsLV1OcEsu7V8Re 31 | bdN0nyNzsyw4C2DcyNDD4SG39PnBMV0JSeSIrAhJm+Ca71KmMqS0kklYqXUcScop 32 | EvYHNJf4EBxHd2BMSMwSDCQfnNXR3b5ddKVUQsgXl9HVnWVZGXo6IGAIVGZCQ367 33 | yhuGfJKXxyR0NHSowk1/MHWv1/R3pjhEnW8zccyWUhG+LB2ufKDSwaV5jmcAEQEA 34 | AYkBJQQYAQIADwUCVlWsMwIbIAUJFo5qAAAKCRBNF8mVzZd18hqLCACCeF+ySpKK 35 | 30DyfDJ26wRjmx6OQigz5ZdP+qmuavyajDFnforKZh4iOfScN/jMvRh20WKHkmaz 36 | OWG4HgvnLeWj3DMxTpP46wH4XWgC+XQ1jeWMi4fkUa3E8JQiPS970miaUXKakhSE 37 | z+pfY6uf9Ay5FBgTqg2zAmCA6yAzMogqQRKi8yvR9MWCbEAJtuTcR3fi3d61dsko 38 | hKuiNfXDlFt3+aTr00lEqqASPy2cguj97kfycT/ANfpYI7iN2DkgR9EOGx0H1WOx 39 | fc5eEtQViqAu2qrnUOEpsoCBOr7pktv/MWHMwJx72E3L5qhjjC872dWPU2cH5Y0y 40 | n7BVBdxwDVJQ 41 | =qw93 42 | -----END PGP PUBLIC KEY BLOCK----- 43 | -------------------------------------------------------------------------------- /debian/control: -------------------------------------------------------------------------------- 1 | Source: python-acme 2 | Maintainer: Debian Let's Encrypt 3 | Uploaders: Harlan Lieberman-Berg 4 | Section: python 5 | Testsuite: autopkgtest-pkg-python 6 | Priority: optional 7 | Build-Depends: debhelper-compat (= 12), 8 | dh-python, 9 | python3, 10 | python3-cryptography (>= 1.3.4), 11 | python3-docutils, 12 | python3-idna (>= 2.5), 13 | python3-idna (<< 2.8), 14 | python3-josepy, 15 | python3-mock, 16 | python3-ndg-httpsclient, 17 | python3-openssl (>= 0.15), 18 | python3-pytest, 19 | python3-requests, 20 | python3-requests-toolbelt, 21 | python3-rfc3339, 22 | python3-setuptools (>= 11.3), 23 | python3-six (>= 1.9), 24 | python3-sphinx (>= 1.3.1-1~), 25 | python3-sphinx-rtd-theme, 26 | python3-tz 27 | Standards-Version: 4.5.0 28 | Vcs-Browser: https://salsa.debian.org/letsencrypt-team/certbot/acme 29 | Vcs-Git: https://salsa.debian.org/letsencrypt-team/certbot/acme.git 30 | Homepage: https://letsencrypt.org/ 31 | Rules-Requires-Root: no 32 | 33 | Package: python3-acme 34 | Architecture: all 35 | Depends: ca-certificates, 36 | python3-openssl (>= 0.15), 37 | ${misc:Depends}, 38 | ${python3:Depends} 39 | Suggests: python-acme-doc 40 | Description: ACME protocol library for Python 3 41 | This is a library used by the Let's Encrypt client for the ACME 42 | (Automated Certificate Management Environment). The ACME protocol is 43 | designed as part of the Let's Encrypt project, to make it possible to 44 | setup an HTTPS server and have it automatically obtain a 45 | browser-trusted certificate, without any human intervention. This 46 | library implements the protocol used for proving the control of a 47 | domain. This library is Python 3. 48 | 49 | Package: python-acme-doc 50 | Architecture: all 51 | Section: doc 52 | Depends: ${misc:Depends}, 53 | ${sphinxdoc:Depends} 54 | Description: ACME protocol library for Python 3 - Documentation 55 | This is a library used by the Let's Encrypt client for the ACME 56 | (Automated Certificate Management Environment). The ACME protocol is 57 | designed as part of the Let's Encrypt project, to make it possible to 58 | setup an HTTPS server and have it automatically obtain a 59 | browser-trusted certificate, without any human intervention. This 60 | library implements the protocol used for proving the control of a 61 | domain. 62 | . 63 | This package provides the documentation. 64 | -------------------------------------------------------------------------------- /tests/testdata/csr-100sans.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE REQUEST----- 2 | MIIHNTCCBt8CAQAwZDELMAkGA1UECAwCQ0ExFjAUBgNVBAcMDVNhbiBGcmFuY2lz 3 | Y28xJzAlBgNVBAsMHkVsZWN0cm9uaWMgRnJvbnRpZXIgRm91bmRhdGlvbjEUMBIG 4 | A1UEAwwLZXhhbXBsZS5jb20wXDANBgkqhkiG9w0BAQEFAANLADBIAkEArHVztFHt 5 | H92ucFJD/N/HW9AsdRsUuHUBBBDlHwNlRd3fp580rv2+6QWE30cWgdmJS86ObRz6 6 | lUTor4R0T+3C5QIDAQABoIIGFDCCBhAGCSqGSIb3DQEJDjGCBgEwggX9MAkGA1Ud 7 | EwQCMAAwCwYDVR0PBAQDAgXgMIIF4QYDVR0RBIIF2DCCBdSCDGV4YW1wbGUxLmNv 8 | bYIMZXhhbXBsZTIuY29tggxleGFtcGxlMy5jb22CDGV4YW1wbGU0LmNvbYIMZXhh 9 | bXBsZTUuY29tggxleGFtcGxlNi5jb22CDGV4YW1wbGU3LmNvbYIMZXhhbXBsZTgu 10 | Y29tggxleGFtcGxlOS5jb22CDWV4YW1wbGUxMC5jb22CDWV4YW1wbGUxMS5jb22C 11 | DWV4YW1wbGUxMi5jb22CDWV4YW1wbGUxMy5jb22CDWV4YW1wbGUxNC5jb22CDWV4 12 | YW1wbGUxNS5jb22CDWV4YW1wbGUxNi5jb22CDWV4YW1wbGUxNy5jb22CDWV4YW1w 13 | bGUxOC5jb22CDWV4YW1wbGUxOS5jb22CDWV4YW1wbGUyMC5jb22CDWV4YW1wbGUy 14 | MS5jb22CDWV4YW1wbGUyMi5jb22CDWV4YW1wbGUyMy5jb22CDWV4YW1wbGUyNC5j 15 | b22CDWV4YW1wbGUyNS5jb22CDWV4YW1wbGUyNi5jb22CDWV4YW1wbGUyNy5jb22C 16 | DWV4YW1wbGUyOC5jb22CDWV4YW1wbGUyOS5jb22CDWV4YW1wbGUzMC5jb22CDWV4 17 | YW1wbGUzMS5jb22CDWV4YW1wbGUzMi5jb22CDWV4YW1wbGUzMy5jb22CDWV4YW1w 18 | bGUzNC5jb22CDWV4YW1wbGUzNS5jb22CDWV4YW1wbGUzNi5jb22CDWV4YW1wbGUz 19 | Ny5jb22CDWV4YW1wbGUzOC5jb22CDWV4YW1wbGUzOS5jb22CDWV4YW1wbGU0MC5j 20 | b22CDWV4YW1wbGU0MS5jb22CDWV4YW1wbGU0Mi5jb22CDWV4YW1wbGU0My5jb22C 21 | DWV4YW1wbGU0NC5jb22CDWV4YW1wbGU0NS5jb22CDWV4YW1wbGU0Ni5jb22CDWV4 22 | YW1wbGU0Ny5jb22CDWV4YW1wbGU0OC5jb22CDWV4YW1wbGU0OS5jb22CDWV4YW1w 23 | bGU1MC5jb22CDWV4YW1wbGU1MS5jb22CDWV4YW1wbGU1Mi5jb22CDWV4YW1wbGU1 24 | My5jb22CDWV4YW1wbGU1NC5jb22CDWV4YW1wbGU1NS5jb22CDWV4YW1wbGU1Ni5j 25 | b22CDWV4YW1wbGU1Ny5jb22CDWV4YW1wbGU1OC5jb22CDWV4YW1wbGU1OS5jb22C 26 | DWV4YW1wbGU2MC5jb22CDWV4YW1wbGU2MS5jb22CDWV4YW1wbGU2Mi5jb22CDWV4 27 | YW1wbGU2My5jb22CDWV4YW1wbGU2NC5jb22CDWV4YW1wbGU2NS5jb22CDWV4YW1w 28 | bGU2Ni5jb22CDWV4YW1wbGU2Ny5jb22CDWV4YW1wbGU2OC5jb22CDWV4YW1wbGU2 29 | OS5jb22CDWV4YW1wbGU3MC5jb22CDWV4YW1wbGU3MS5jb22CDWV4YW1wbGU3Mi5j 30 | b22CDWV4YW1wbGU3My5jb22CDWV4YW1wbGU3NC5jb22CDWV4YW1wbGU3NS5jb22C 31 | DWV4YW1wbGU3Ni5jb22CDWV4YW1wbGU3Ny5jb22CDWV4YW1wbGU3OC5jb22CDWV4 32 | YW1wbGU3OS5jb22CDWV4YW1wbGU4MC5jb22CDWV4YW1wbGU4MS5jb22CDWV4YW1w 33 | bGU4Mi5jb22CDWV4YW1wbGU4My5jb22CDWV4YW1wbGU4NC5jb22CDWV4YW1wbGU4 34 | NS5jb22CDWV4YW1wbGU4Ni5jb22CDWV4YW1wbGU4Ny5jb22CDWV4YW1wbGU4OC5j 35 | b22CDWV4YW1wbGU4OS5jb22CDWV4YW1wbGU5MC5jb22CDWV4YW1wbGU5MS5jb22C 36 | DWV4YW1wbGU5Mi5jb22CDWV4YW1wbGU5My5jb22CDWV4YW1wbGU5NC5jb22CDWV4 37 | YW1wbGU5NS5jb22CDWV4YW1wbGU5Ni5jb22CDWV4YW1wbGU5Ny5jb22CDWV4YW1w 38 | bGU5OC5jb22CDWV4YW1wbGU5OS5jb22CDmV4YW1wbGUxMDAuY29tMA0GCSqGSIb3 39 | DQEBCwUAA0EAW05UMFavHn2rkzMyUfzsOvWzVNlm43eO2yHu5h5TzDb23gkDnNEo 40 | duUAbQ+CLJHYd+MvRCmPQ+3ZnaPy7l/0Hg== 41 | -----END CERTIFICATE REQUEST----- 42 | -------------------------------------------------------------------------------- /tests/testdata/cert-100sans.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIHxDCCB26gAwIBAgIJAOGrG1Un9lHiMA0GCSqGSIb3DQEBCwUAMGQxCzAJBgNV 3 | BAgMAkNBMRYwFAYDVQQHDA1TYW4gRnJhbmNpc2NvMScwJQYDVQQLDB5FbGVjdHJv 4 | bmljIEZyb250aWVyIEZvdW5kYXRpb24xFDASBgNVBAMMC2V4YW1wbGUuY29tMB4X 5 | DTE2MDEwNjE5MDkzN1oXDTE2MDEwNzE5MDkzN1owZDELMAkGA1UECAwCQ0ExFjAU 6 | BgNVBAcMDVNhbiBGcmFuY2lzY28xJzAlBgNVBAsMHkVsZWN0cm9uaWMgRnJvbnRp 7 | ZXIgRm91bmRhdGlvbjEUMBIGA1UEAwwLZXhhbXBsZS5jb20wXDANBgkqhkiG9w0B 8 | AQEFAANLADBIAkEArHVztFHtH92ucFJD/N/HW9AsdRsUuHUBBBDlHwNlRd3fp580 9 | rv2+6QWE30cWgdmJS86ObRz6lUTor4R0T+3C5QIDAQABo4IGATCCBf0wCQYDVR0T 10 | BAIwADALBgNVHQ8EBAMCBeAwggXhBgNVHREEggXYMIIF1IIMZXhhbXBsZTEuY29t 11 | ggxleGFtcGxlMi5jb22CDGV4YW1wbGUzLmNvbYIMZXhhbXBsZTQuY29tggxleGFt 12 | cGxlNS5jb22CDGV4YW1wbGU2LmNvbYIMZXhhbXBsZTcuY29tggxleGFtcGxlOC5j 13 | b22CDGV4YW1wbGU5LmNvbYINZXhhbXBsZTEwLmNvbYINZXhhbXBsZTExLmNvbYIN 14 | ZXhhbXBsZTEyLmNvbYINZXhhbXBsZTEzLmNvbYINZXhhbXBsZTE0LmNvbYINZXhh 15 | bXBsZTE1LmNvbYINZXhhbXBsZTE2LmNvbYINZXhhbXBsZTE3LmNvbYINZXhhbXBs 16 | ZTE4LmNvbYINZXhhbXBsZTE5LmNvbYINZXhhbXBsZTIwLmNvbYINZXhhbXBsZTIx 17 | LmNvbYINZXhhbXBsZTIyLmNvbYINZXhhbXBsZTIzLmNvbYINZXhhbXBsZTI0LmNv 18 | bYINZXhhbXBsZTI1LmNvbYINZXhhbXBsZTI2LmNvbYINZXhhbXBsZTI3LmNvbYIN 19 | ZXhhbXBsZTI4LmNvbYINZXhhbXBsZTI5LmNvbYINZXhhbXBsZTMwLmNvbYINZXhh 20 | bXBsZTMxLmNvbYINZXhhbXBsZTMyLmNvbYINZXhhbXBsZTMzLmNvbYINZXhhbXBs 21 | ZTM0LmNvbYINZXhhbXBsZTM1LmNvbYINZXhhbXBsZTM2LmNvbYINZXhhbXBsZTM3 22 | LmNvbYINZXhhbXBsZTM4LmNvbYINZXhhbXBsZTM5LmNvbYINZXhhbXBsZTQwLmNv 23 | bYINZXhhbXBsZTQxLmNvbYINZXhhbXBsZTQyLmNvbYINZXhhbXBsZTQzLmNvbYIN 24 | ZXhhbXBsZTQ0LmNvbYINZXhhbXBsZTQ1LmNvbYINZXhhbXBsZTQ2LmNvbYINZXhh 25 | bXBsZTQ3LmNvbYINZXhhbXBsZTQ4LmNvbYINZXhhbXBsZTQ5LmNvbYINZXhhbXBs 26 | ZTUwLmNvbYINZXhhbXBsZTUxLmNvbYINZXhhbXBsZTUyLmNvbYINZXhhbXBsZTUz 27 | LmNvbYINZXhhbXBsZTU0LmNvbYINZXhhbXBsZTU1LmNvbYINZXhhbXBsZTU2LmNv 28 | bYINZXhhbXBsZTU3LmNvbYINZXhhbXBsZTU4LmNvbYINZXhhbXBsZTU5LmNvbYIN 29 | ZXhhbXBsZTYwLmNvbYINZXhhbXBsZTYxLmNvbYINZXhhbXBsZTYyLmNvbYINZXhh 30 | bXBsZTYzLmNvbYINZXhhbXBsZTY0LmNvbYINZXhhbXBsZTY1LmNvbYINZXhhbXBs 31 | ZTY2LmNvbYINZXhhbXBsZTY3LmNvbYINZXhhbXBsZTY4LmNvbYINZXhhbXBsZTY5 32 | LmNvbYINZXhhbXBsZTcwLmNvbYINZXhhbXBsZTcxLmNvbYINZXhhbXBsZTcyLmNv 33 | bYINZXhhbXBsZTczLmNvbYINZXhhbXBsZTc0LmNvbYINZXhhbXBsZTc1LmNvbYIN 34 | ZXhhbXBsZTc2LmNvbYINZXhhbXBsZTc3LmNvbYINZXhhbXBsZTc4LmNvbYINZXhh 35 | bXBsZTc5LmNvbYINZXhhbXBsZTgwLmNvbYINZXhhbXBsZTgxLmNvbYINZXhhbXBs 36 | ZTgyLmNvbYINZXhhbXBsZTgzLmNvbYINZXhhbXBsZTg0LmNvbYINZXhhbXBsZTg1 37 | LmNvbYINZXhhbXBsZTg2LmNvbYINZXhhbXBsZTg3LmNvbYINZXhhbXBsZTg4LmNv 38 | bYINZXhhbXBsZTg5LmNvbYINZXhhbXBsZTkwLmNvbYINZXhhbXBsZTkxLmNvbYIN 39 | ZXhhbXBsZTkyLmNvbYINZXhhbXBsZTkzLmNvbYINZXhhbXBsZTk0LmNvbYINZXhh 40 | bXBsZTk1LmNvbYINZXhhbXBsZTk2LmNvbYINZXhhbXBsZTk3LmNvbYINZXhhbXBs 41 | ZTk4LmNvbYINZXhhbXBsZTk5LmNvbYIOZXhhbXBsZTEwMC5jb20wDQYJKoZIhvcN 42 | AQELBQADQQBEunJbKUXcyNKTSfA0pKRyWNiKmkoBqYgfZS6eHNrNH/hjFzHtzyDQ 43 | XYHHK6kgEWBvHfRXGmqhFvht+b1tQKkG 44 | -----END CERTIFICATE----- 45 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | from setuptools import find_packages 4 | from setuptools import setup 5 | from setuptools.command.test import test as TestCommand 6 | 7 | version = '1.1.0' 8 | 9 | # Please update tox.ini when modifying dependency version requirements 10 | install_requires = [ 11 | # load_pem_private/public_key (>=0.6) 12 | # rsa_recover_prime_factors (>=0.8) 13 | 'cryptography>=1.2.3', 14 | # formerly known as acme.jose: 15 | # 1.1.0+ is required to avoid the warnings described at 16 | # https://github.com/certbot/josepy/issues/13. 17 | 'josepy>=1.1.0', 18 | 'mock', 19 | # Connection.set_tlsext_host_name (>=0.13) 20 | 'PyOpenSSL>=0.13.1', 21 | 'pyrfc3339', 22 | 'pytz', 23 | 'requests[security]>=2.6.0', # security extras added in 2.4.1 24 | 'requests-toolbelt>=0.3.0', 25 | 'setuptools', 26 | 'six>=1.9.0', # needed for python_2_unicode_compatible 27 | ] 28 | 29 | dev_extras = [ 30 | 'pytest', 31 | 'pytest-xdist', 32 | 'tox', 33 | ] 34 | 35 | docs_extras = [ 36 | 'Sphinx>=1.0', # autodoc_member_order = 'bysource', autodoc_default_flags 37 | 'sphinx_rtd_theme', 38 | ] 39 | 40 | 41 | class PyTest(TestCommand): 42 | user_options = [] 43 | 44 | def initialize_options(self): 45 | TestCommand.initialize_options(self) 46 | self.pytest_args = '' 47 | 48 | def run_tests(self): 49 | import shlex 50 | # import here, cause outside the eggs aren't loaded 51 | import pytest 52 | errno = pytest.main(shlex.split(self.pytest_args)) 53 | sys.exit(errno) 54 | 55 | 56 | setup( 57 | name='acme', 58 | version=version, 59 | description='ACME protocol implementation in Python', 60 | url='https://github.com/letsencrypt/letsencrypt', 61 | author="Certbot Project", 62 | author_email='client-dev@letsencrypt.org', 63 | license='Apache License 2.0', 64 | python_requires='>=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*', 65 | classifiers=[ 66 | 'Development Status :: 5 - Production/Stable', 67 | 'Intended Audience :: Developers', 68 | 'License :: OSI Approved :: Apache Software License', 69 | 'Programming Language :: Python', 70 | 'Programming Language :: Python :: 2', 71 | 'Programming Language :: Python :: 2.7', 72 | 'Programming Language :: Python :: 3', 73 | 'Programming Language :: Python :: 3.4', 74 | 'Programming Language :: Python :: 3.5', 75 | 'Programming Language :: Python :: 3.6', 76 | 'Programming Language :: Python :: 3.7', 77 | 'Programming Language :: Python :: 3.8', 78 | 'Topic :: Internet :: WWW/HTTP', 79 | 'Topic :: Security', 80 | ], 81 | 82 | packages=find_packages(), 83 | include_package_data=True, 84 | install_requires=install_requires, 85 | extras_require={ 86 | 'dev': dev_extras, 87 | 'docs': docs_extras, 88 | }, 89 | test_suite='acme', 90 | tests_require=["pytest"], 91 | cmdclass={"test": PyTest}, 92 | ) 93 | -------------------------------------------------------------------------------- /acme/errors.py: -------------------------------------------------------------------------------- 1 | """ACME errors.""" 2 | from josepy import errors as jose_errors 3 | 4 | 5 | class Error(Exception): 6 | """Generic ACME error.""" 7 | 8 | 9 | class DependencyError(Error): 10 | """Dependency error""" 11 | 12 | 13 | class SchemaValidationError(jose_errors.DeserializationError): 14 | """JSON schema ACME object validation error.""" 15 | 16 | 17 | class ClientError(Error): 18 | """Network error.""" 19 | 20 | 21 | class UnexpectedUpdate(ClientError): 22 | """Unexpected update error.""" 23 | 24 | 25 | class NonceError(ClientError): 26 | """Server response nonce error.""" 27 | 28 | 29 | class BadNonce(NonceError): 30 | """Bad nonce error.""" 31 | def __init__(self, nonce, error, *args, **kwargs): 32 | # MyPy complains here that there is too many arguments for BaseException constructor. 33 | # This is an error fixed in typeshed, see https://github.com/python/mypy/issues/4183 34 | # The fix is included in MyPy>=0.740, but upgrading it would bring dozen of errors due to 35 | # new types definitions. So we ignore the error until the code base is fixed to match 36 | # with MyPy>=0.740 referential. 37 | super(BadNonce, self).__init__(*args, **kwargs) # type: ignore 38 | self.nonce = nonce 39 | self.error = error 40 | 41 | def __str__(self): 42 | return 'Invalid nonce ({0!r}): {1}'.format(self.nonce, self.error) 43 | 44 | 45 | class MissingNonce(NonceError): 46 | """Missing nonce error. 47 | 48 | According to the specification an "ACME server MUST include an 49 | Replay-Nonce header field in each successful response to a POST it 50 | provides to a client (...)". 51 | 52 | :ivar requests.Response response: HTTP Response 53 | 54 | """ 55 | def __init__(self, response, *args, **kwargs): 56 | # See comment in BadNonce constructor above for an explanation of type: ignore here. 57 | super(MissingNonce, self).__init__(*args, **kwargs) # type: ignore 58 | self.response = response 59 | 60 | def __str__(self): 61 | return ('Server {0} response did not include a replay ' 62 | 'nonce, headers: {1} (This may be a service outage)'.format( 63 | self.response.request.method, self.response.headers)) 64 | 65 | 66 | class PollError(ClientError): 67 | """Generic error when polling for authorization fails. 68 | 69 | This might be caused by either timeout (`exhausted` will be non-empty) 70 | or by some authorization being invalid. 71 | 72 | :ivar exhausted: Set of `.AuthorizationResource` that didn't finish 73 | within max allowed attempts. 74 | :ivar updated: Mapping from original `.AuthorizationResource` 75 | to the most recently updated one 76 | 77 | """ 78 | def __init__(self, exhausted, updated): 79 | self.exhausted = exhausted 80 | self.updated = updated 81 | super(PollError, self).__init__() 82 | 83 | @property 84 | def timeout(self): 85 | """Was the error caused by timeout?""" 86 | return bool(self.exhausted) 87 | 88 | def __repr__(self): 89 | return '{0}(exhausted={1!r}, updated={2!r})'.format( 90 | self.__class__.__name__, self.exhausted, self.updated) 91 | 92 | 93 | class ValidationError(Error): 94 | """Error for authorization failures. Contains a list of authorization 95 | resources, each of which is invalid and should have an error field. 96 | """ 97 | def __init__(self, failed_authzrs): 98 | self.failed_authzrs = failed_authzrs 99 | super(ValidationError, self).__init__() 100 | 101 | 102 | class TimeoutError(Error): # pylint: disable=redefined-builtin 103 | """Error for when polling an authorization or an order times out.""" 104 | 105 | 106 | class IssuanceError(Error): 107 | """Error sent by the server after requesting issuance of a certificate.""" 108 | 109 | def __init__(self, error): 110 | """Initialize. 111 | 112 | :param messages.Error error: The error provided by the server. 113 | """ 114 | self.error = error 115 | super(IssuanceError, self).__init__() 116 | 117 | 118 | class ConflictError(ClientError): 119 | """Error for when the server returns a 409 (Conflict) HTTP status. 120 | 121 | In the version of ACME implemented by Boulder, this is used to find an 122 | account if you only have the private key, but don't know the account URL. 123 | 124 | Also used in V2 of the ACME client for the same purpose. 125 | """ 126 | def __init__(self, location): 127 | self.location = location 128 | super(ConflictError, self).__init__() 129 | 130 | 131 | class WildcardUnsupportedError(Error): 132 | """Error for when a wildcard is requested but is unsupported by ACME CA.""" 133 | -------------------------------------------------------------------------------- /tests/standalone_test.py: -------------------------------------------------------------------------------- 1 | """Tests for acme.standalone.""" 2 | import socket 3 | import threading 4 | import unittest 5 | 6 | import josepy as jose 7 | import mock 8 | import requests 9 | from six.moves import http_client # pylint: disable=import-error 10 | from six.moves import socketserver # type: ignore # pylint: disable=import-error 11 | 12 | from acme import challenges 13 | from acme.magic_typing import Set # pylint: disable=unused-import, no-name-in-module 14 | import test_util 15 | 16 | 17 | class TLSServerTest(unittest.TestCase): 18 | """Tests for acme.standalone.TLSServer.""" 19 | 20 | 21 | def test_bind(self): # pylint: disable=no-self-use 22 | from acme.standalone import TLSServer 23 | server = TLSServer( 24 | ('', 0), socketserver.BaseRequestHandler, bind_and_activate=True) 25 | server.server_close() 26 | 27 | def test_ipv6(self): 28 | if socket.has_ipv6: 29 | from acme.standalone import TLSServer 30 | server = TLSServer( 31 | ('', 0), socketserver.BaseRequestHandler, bind_and_activate=True, ipv6=True) 32 | server.server_close() 33 | 34 | 35 | class HTTP01ServerTest(unittest.TestCase): 36 | """Tests for acme.standalone.HTTP01Server.""" 37 | 38 | 39 | def setUp(self): 40 | self.account_key = jose.JWK.load( 41 | test_util.load_vector('rsa1024_key.pem')) 42 | self.resources = set() # type: Set 43 | 44 | from acme.standalone import HTTP01Server 45 | self.server = HTTP01Server(('', 0), resources=self.resources) 46 | 47 | self.port = self.server.socket.getsockname()[1] 48 | self.thread = threading.Thread(target=self.server.serve_forever) 49 | self.thread.start() 50 | 51 | def tearDown(self): 52 | self.server.shutdown() 53 | self.thread.join() 54 | 55 | def test_index(self): 56 | response = requests.get( 57 | 'http://localhost:{0}'.format(self.port), verify=False) 58 | self.assertEqual( 59 | response.text, 'ACME client standalone challenge solver') 60 | self.assertTrue(response.ok) 61 | 62 | def test_404(self): 63 | response = requests.get( 64 | 'http://localhost:{0}/foo'.format(self.port), verify=False) 65 | self.assertEqual(response.status_code, http_client.NOT_FOUND) 66 | 67 | def _test_http01(self, add): 68 | chall = challenges.HTTP01(token=(b'x' * 16)) 69 | response, validation = chall.response_and_validation(self.account_key) 70 | 71 | from acme.standalone import HTTP01RequestHandler 72 | resource = HTTP01RequestHandler.HTTP01Resource( 73 | chall=chall, response=response, validation=validation) 74 | if add: 75 | self.resources.add(resource) 76 | return resource.response.simple_verify( 77 | resource.chall, 'localhost', self.account_key.public_key(), 78 | port=self.port) 79 | 80 | def test_http01_found(self): 81 | self.assertTrue(self._test_http01(add=True)) 82 | 83 | def test_http01_not_found(self): 84 | self.assertFalse(self._test_http01(add=False)) 85 | 86 | 87 | class BaseDualNetworkedServersTest(unittest.TestCase): 88 | """Test for acme.standalone.BaseDualNetworkedServers.""" 89 | 90 | 91 | class SingleProtocolServer(socketserver.TCPServer): 92 | """Server that only serves on a single protocol. FreeBSD has this behavior for AF_INET6.""" 93 | def __init__(self, *args, **kwargs): 94 | ipv6 = kwargs.pop("ipv6", False) 95 | if ipv6: 96 | self.address_family = socket.AF_INET6 97 | kwargs["bind_and_activate"] = False 98 | else: 99 | self.address_family = socket.AF_INET 100 | socketserver.TCPServer.__init__(self, *args, **kwargs) 101 | if ipv6: 102 | # NB: On Windows, socket.IPPROTO_IPV6 constant may be missing. 103 | # We use the corresponding value (41) instead. 104 | level = getattr(socket, "IPPROTO_IPV6", 41) 105 | self.socket.setsockopt(level, socket.IPV6_V6ONLY, 1) 106 | try: 107 | self.server_bind() 108 | self.server_activate() 109 | except: 110 | self.server_close() 111 | raise 112 | 113 | @mock.patch("socket.socket.bind") 114 | def test_fail_to_bind(self, mock_bind): 115 | mock_bind.side_effect = socket.error 116 | from acme.standalone import BaseDualNetworkedServers 117 | self.assertRaises(socket.error, BaseDualNetworkedServers, 118 | BaseDualNetworkedServersTest.SingleProtocolServer, 119 | ('', 0), 120 | socketserver.BaseRequestHandler) 121 | 122 | def test_ports_equal(self): 123 | from acme.standalone import BaseDualNetworkedServers 124 | servers = BaseDualNetworkedServers( 125 | BaseDualNetworkedServersTest.SingleProtocolServer, 126 | ('', 0), 127 | socketserver.BaseRequestHandler) 128 | socknames = servers.getsocknames() 129 | prev_port = None 130 | # assert ports are equal 131 | for sockname in socknames: 132 | port = sockname[1] 133 | if prev_port: 134 | self.assertEqual(prev_port, port) 135 | prev_port = port 136 | 137 | 138 | class HTTP01DualNetworkedServersTest(unittest.TestCase): 139 | """Tests for acme.standalone.HTTP01DualNetworkedServers.""" 140 | 141 | 142 | def setUp(self): 143 | self.account_key = jose.JWK.load( 144 | test_util.load_vector('rsa1024_key.pem')) 145 | self.resources = set() # type: Set 146 | 147 | from acme.standalone import HTTP01DualNetworkedServers 148 | self.servers = HTTP01DualNetworkedServers(('', 0), resources=self.resources) 149 | 150 | self.port = self.servers.getsocknames()[0][1] 151 | self.servers.serve_forever() 152 | 153 | def tearDown(self): 154 | self.servers.shutdown_and_server_close() 155 | 156 | def test_index(self): 157 | response = requests.get( 158 | 'http://localhost:{0}'.format(self.port), verify=False) 159 | self.assertEqual( 160 | response.text, 'ACME client standalone challenge solver') 161 | self.assertTrue(response.ok) 162 | 163 | def test_404(self): 164 | response = requests.get( 165 | 'http://localhost:{0}/foo'.format(self.port), verify=False) 166 | self.assertEqual(response.status_code, http_client.NOT_FOUND) 167 | 168 | def _test_http01(self, add): 169 | chall = challenges.HTTP01(token=(b'x' * 16)) 170 | response, validation = chall.response_and_validation(self.account_key) 171 | 172 | from acme.standalone import HTTP01RequestHandler 173 | resource = HTTP01RequestHandler.HTTP01Resource( 174 | chall=chall, response=response, validation=validation) 175 | if add: 176 | self.resources.add(resource) 177 | return resource.response.simple_verify( 178 | resource.chall, 'localhost', self.account_key.public_key(), 179 | port=self.port) 180 | 181 | def test_http01_found(self): 182 | self.assertTrue(self._test_http01(add=True)) 183 | 184 | def test_http01_not_found(self): 185 | self.assertFalse(self._test_http01(add=False)) 186 | 187 | 188 | if __name__ == "__main__": 189 | unittest.main() # pragma: no cover 190 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | PAPER = 8 | BUILDDIR = _build 9 | 10 | # User-friendly check for sphinx-build 11 | ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1) 12 | $(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/) 13 | endif 14 | 15 | # Internal variables. 16 | PAPEROPT_a4 = -D latex_paper_size=a4 17 | PAPEROPT_letter = -D latex_paper_size=letter 18 | ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 19 | # the i18n builder cannot share the environment and doctrees with the others 20 | I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 21 | 22 | .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest coverage gettext 23 | 24 | help: 25 | @echo "Please use \`make ' where is one of" 26 | @echo " html to make standalone HTML files" 27 | @echo " dirhtml to make HTML files named index.html in directories" 28 | @echo " singlehtml to make a single large HTML file" 29 | @echo " pickle to make pickle files" 30 | @echo " json to make JSON files" 31 | @echo " htmlhelp to make HTML files and a HTML help project" 32 | @echo " qthelp to make HTML files and a qthelp project" 33 | @echo " applehelp to make an Apple Help Book" 34 | @echo " devhelp to make HTML files and a Devhelp project" 35 | @echo " epub to make an epub" 36 | @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" 37 | @echo " latexpdf to make LaTeX files and run them through pdflatex" 38 | @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx" 39 | @echo " text to make text files" 40 | @echo " man to make manual pages" 41 | @echo " texinfo to make Texinfo files" 42 | @echo " info to make Texinfo files and run them through makeinfo" 43 | @echo " gettext to make PO message catalogs" 44 | @echo " changes to make an overview of all changed/added/deprecated items" 45 | @echo " xml to make Docutils-native XML files" 46 | @echo " pseudoxml to make pseudoxml-XML files for display purposes" 47 | @echo " linkcheck to check all external links for integrity" 48 | @echo " doctest to run all doctests embedded in the documentation (if enabled)" 49 | @echo " coverage to run coverage check of the documentation (if enabled)" 50 | 51 | clean: 52 | rm -rf $(BUILDDIR)/* 53 | 54 | html: 55 | $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html 56 | @echo 57 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." 58 | 59 | dirhtml: 60 | $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml 61 | @echo 62 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." 63 | 64 | singlehtml: 65 | $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml 66 | @echo 67 | @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." 68 | 69 | pickle: 70 | $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle 71 | @echo 72 | @echo "Build finished; now you can process the pickle files." 73 | 74 | json: 75 | $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json 76 | @echo 77 | @echo "Build finished; now you can process the JSON files." 78 | 79 | htmlhelp: 80 | $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp 81 | @echo 82 | @echo "Build finished; now you can run HTML Help Workshop with the" \ 83 | ".hhp project file in $(BUILDDIR)/htmlhelp." 84 | 85 | qthelp: 86 | $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp 87 | @echo 88 | @echo "Build finished; now you can run "qcollectiongenerator" with the" \ 89 | ".qhcp project file in $(BUILDDIR)/qthelp, like this:" 90 | @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/acme-python.qhcp" 91 | @echo "To view the help file:" 92 | @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/acme-python.qhc" 93 | 94 | applehelp: 95 | $(SPHINXBUILD) -b applehelp $(ALLSPHINXOPTS) $(BUILDDIR)/applehelp 96 | @echo 97 | @echo "Build finished. The help book is in $(BUILDDIR)/applehelp." 98 | @echo "N.B. You won't be able to view it unless you put it in" \ 99 | "~/Library/Documentation/Help or install it in your application" \ 100 | "bundle." 101 | 102 | devhelp: 103 | $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp 104 | @echo 105 | @echo "Build finished." 106 | @echo "To view the help file:" 107 | @echo "# mkdir -p $$HOME/.local/share/devhelp/acme-python" 108 | @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/acme-python" 109 | @echo "# devhelp" 110 | 111 | epub: 112 | $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub 113 | @echo 114 | @echo "Build finished. The epub file is in $(BUILDDIR)/epub." 115 | 116 | latex: 117 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 118 | @echo 119 | @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." 120 | @echo "Run \`make' in that directory to run these through (pdf)latex" \ 121 | "(use \`make latexpdf' here to do that automatically)." 122 | 123 | latexpdf: 124 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 125 | @echo "Running LaTeX files through pdflatex..." 126 | $(MAKE) -C $(BUILDDIR)/latex all-pdf 127 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 128 | 129 | latexpdfja: 130 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 131 | @echo "Running LaTeX files through platex and dvipdfmx..." 132 | $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja 133 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 134 | 135 | text: 136 | $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text 137 | @echo 138 | @echo "Build finished. The text files are in $(BUILDDIR)/text." 139 | 140 | man: 141 | $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man 142 | @echo 143 | @echo "Build finished. The manual pages are in $(BUILDDIR)/man." 144 | 145 | texinfo: 146 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 147 | @echo 148 | @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." 149 | @echo "Run \`make' in that directory to run these through makeinfo" \ 150 | "(use \`make info' here to do that automatically)." 151 | 152 | info: 153 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 154 | @echo "Running Texinfo files through makeinfo..." 155 | make -C $(BUILDDIR)/texinfo info 156 | @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." 157 | 158 | gettext: 159 | $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale 160 | @echo 161 | @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." 162 | 163 | changes: 164 | $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes 165 | @echo 166 | @echo "The overview file is in $(BUILDDIR)/changes." 167 | 168 | linkcheck: 169 | $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck 170 | @echo 171 | @echo "Link check complete; look for any errors in the above output " \ 172 | "or in $(BUILDDIR)/linkcheck/output.txt." 173 | 174 | doctest: 175 | $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest 176 | @echo "Testing of doctests in the sources finished, look at the " \ 177 | "results in $(BUILDDIR)/doctest/output.txt." 178 | 179 | coverage: 180 | $(SPHINXBUILD) -b coverage $(ALLSPHINXOPTS) $(BUILDDIR)/coverage 181 | @echo "Testing of coverage in the sources finished, look at the " \ 182 | "results in $(BUILDDIR)/coverage/python.txt." 183 | 184 | xml: 185 | $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml 186 | @echo 187 | @echo "Build finished. The XML files are in $(BUILDDIR)/xml." 188 | 189 | pseudoxml: 190 | $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml 191 | @echo 192 | @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml." 193 | -------------------------------------------------------------------------------- /examples/http01_example.py: -------------------------------------------------------------------------------- 1 | """Example ACME-V2 API for HTTP-01 challenge. 2 | 3 | Brief: 4 | 5 | This a complete usage example of the python-acme API. 6 | 7 | Limitations of this example: 8 | - Works for only one Domain name 9 | - Performs only HTTP-01 challenge 10 | - Uses ACME-v2 11 | 12 | Workflow: 13 | (Account creation) 14 | - Create account key 15 | - Register account and accept TOS 16 | (Certificate actions) 17 | - Select HTTP-01 within offered challenges by the CA server 18 | - Set up http challenge resource 19 | - Set up standalone web server 20 | - Create domain private key and CSR 21 | - Issue certificate 22 | - Renew certificate 23 | - Revoke certificate 24 | (Account update actions) 25 | - Change contact information 26 | - Deactivate Account 27 | """ 28 | from contextlib import contextmanager 29 | 30 | from cryptography.hazmat.backends import default_backend 31 | from cryptography.hazmat.primitives.asymmetric import rsa 32 | import josepy as jose 33 | import OpenSSL 34 | 35 | from acme import challenges 36 | from acme import client 37 | from acme import crypto_util 38 | from acme import errors 39 | from acme import messages 40 | from acme import standalone 41 | 42 | # Constants: 43 | 44 | # This is the staging point for ACME-V2 within Let's Encrypt. 45 | DIRECTORY_URL = 'https://acme-staging-v02.api.letsencrypt.org/directory' 46 | 47 | USER_AGENT = 'python-acme-example' 48 | 49 | # Account key size 50 | ACC_KEY_BITS = 2048 51 | 52 | # Certificate private key size 53 | CERT_PKEY_BITS = 2048 54 | 55 | # Domain name for the certificate. 56 | DOMAIN = 'client.example.com' 57 | 58 | # If you are running Boulder locally, it is possible to configure any port 59 | # number to execute the challenge, but real CA servers will always use port 60 | # 80, as described in the ACME specification. 61 | PORT = 80 62 | 63 | 64 | # Useful methods and classes: 65 | 66 | 67 | def new_csr_comp(domain_name, pkey_pem=None): 68 | """Create certificate signing request.""" 69 | if pkey_pem is None: 70 | # Create private key. 71 | pkey = OpenSSL.crypto.PKey() 72 | pkey.generate_key(OpenSSL.crypto.TYPE_RSA, CERT_PKEY_BITS) 73 | pkey_pem = OpenSSL.crypto.dump_privatekey(OpenSSL.crypto.FILETYPE_PEM, 74 | pkey) 75 | csr_pem = crypto_util.make_csr(pkey_pem, [domain_name]) 76 | return pkey_pem, csr_pem 77 | 78 | 79 | def select_http01_chall(orderr): 80 | """Extract authorization resource from within order resource.""" 81 | # Authorization Resource: authz. 82 | # This object holds the offered challenges by the server and their status. 83 | authz_list = orderr.authorizations 84 | 85 | for authz in authz_list: 86 | # Choosing challenge. 87 | # authz.body.challenges is a set of ChallengeBody objects. 88 | for i in authz.body.challenges: 89 | # Find the supported challenge. 90 | if isinstance(i.chall, challenges.HTTP01): 91 | return i 92 | 93 | raise Exception('HTTP-01 challenge was not offered by the CA server.') 94 | 95 | 96 | @contextmanager 97 | def challenge_server(http_01_resources): 98 | """Manage standalone server set up and shutdown.""" 99 | 100 | # Setting up a fake server that binds at PORT and any address. 101 | address = ('', PORT) 102 | try: 103 | servers = standalone.HTTP01DualNetworkedServers(address, 104 | http_01_resources) 105 | # Start client standalone web server. 106 | servers.serve_forever() 107 | yield servers 108 | finally: 109 | # Shutdown client web server and unbind from PORT 110 | servers.shutdown_and_server_close() 111 | 112 | 113 | def perform_http01(client_acme, challb, orderr): 114 | """Set up standalone webserver and perform HTTP-01 challenge.""" 115 | 116 | response, validation = challb.response_and_validation(client_acme.net.key) 117 | 118 | resource = standalone.HTTP01RequestHandler.HTTP01Resource( 119 | chall=challb.chall, response=response, validation=validation) 120 | 121 | with challenge_server({resource}): 122 | # Let the CA server know that we are ready for the challenge. 123 | client_acme.answer_challenge(challb, response) 124 | 125 | # Wait for challenge status and then issue a certificate. 126 | # It is possible to set a deadline time. 127 | finalized_orderr = client_acme.poll_and_finalize(orderr) 128 | 129 | return finalized_orderr.fullchain_pem 130 | 131 | 132 | # Main examples: 133 | 134 | 135 | def example_http(): 136 | """This example executes the whole process of fulfilling a HTTP-01 137 | challenge for one specific domain. 138 | 139 | The workflow consists of: 140 | (Account creation) 141 | - Create account key 142 | - Register account and accept TOS 143 | (Certificate actions) 144 | - Select HTTP-01 within offered challenges by the CA server 145 | - Set up http challenge resource 146 | - Set up standalone web server 147 | - Create domain private key and CSR 148 | - Issue certificate 149 | - Renew certificate 150 | - Revoke certificate 151 | (Account update actions) 152 | - Change contact information 153 | - Deactivate Account 154 | 155 | """ 156 | # Create account key 157 | 158 | acc_key = jose.JWKRSA( 159 | key=rsa.generate_private_key(public_exponent=65537, 160 | key_size=ACC_KEY_BITS, 161 | backend=default_backend())) 162 | 163 | # Register account and accept TOS 164 | 165 | net = client.ClientNetwork(acc_key, user_agent=USER_AGENT) 166 | directory = messages.Directory.from_json(net.get(DIRECTORY_URL).json()) 167 | client_acme = client.ClientV2(directory, net=net) 168 | 169 | # Terms of Service URL is in client_acme.directory.meta.terms_of_service 170 | # Registration Resource: regr 171 | # Creates account with contact information. 172 | email = ('fake@example.com') 173 | regr = client_acme.new_account( 174 | messages.NewRegistration.from_data( 175 | email=email, terms_of_service_agreed=True)) 176 | 177 | # Create domain private key and CSR 178 | pkey_pem, csr_pem = new_csr_comp(DOMAIN) 179 | 180 | # Issue certificate 181 | 182 | orderr = client_acme.new_order(csr_pem) 183 | 184 | # Select HTTP-01 within offered challenges by the CA server 185 | challb = select_http01_chall(orderr) 186 | 187 | # The certificate is ready to be used in the variable "fullchain_pem". 188 | fullchain_pem = perform_http01(client_acme, challb, orderr) 189 | 190 | # Renew certificate 191 | 192 | _, csr_pem = new_csr_comp(DOMAIN, pkey_pem) 193 | 194 | orderr = client_acme.new_order(csr_pem) 195 | 196 | challb = select_http01_chall(orderr) 197 | 198 | # Performing challenge 199 | fullchain_pem = perform_http01(client_acme, challb, orderr) 200 | 201 | # Revoke certificate 202 | 203 | fullchain_com = jose.ComparableX509( 204 | OpenSSL.crypto.load_certificate( 205 | OpenSSL.crypto.FILETYPE_PEM, fullchain_pem)) 206 | 207 | try: 208 | client_acme.revoke(fullchain_com, 0) # revocation reason = 0 209 | except errors.ConflictError: 210 | # Certificate already revoked. 211 | pass 212 | 213 | # Query registration status. 214 | client_acme.net.account = regr 215 | try: 216 | regr = client_acme.query_registration(regr) 217 | except errors.Error as err: 218 | if err.typ == messages.OLD_ERROR_PREFIX + 'unauthorized' \ 219 | or err.typ == messages.ERROR_PREFIX + 'unauthorized': 220 | # Status is deactivated. 221 | pass 222 | raise 223 | 224 | # Change contact information 225 | 226 | email = 'newfake@example.com' 227 | regr = client_acme.update_registration( 228 | regr.update( 229 | body=regr.body.update( 230 | contact=('mailto:' + email,) 231 | ) 232 | ) 233 | ) 234 | 235 | # Deactivate account/registration 236 | 237 | regr = client_acme.deactivate_registration(regr) 238 | 239 | 240 | if __name__ == "__main__": 241 | example_http() 242 | -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | REM Command file for Sphinx documentation 4 | 5 | if "%SPHINXBUILD%" == "" ( 6 | set SPHINXBUILD=sphinx-build 7 | ) 8 | set BUILDDIR=_build 9 | set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% . 10 | set I18NSPHINXOPTS=%SPHINXOPTS% . 11 | if NOT "%PAPER%" == "" ( 12 | set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS% 13 | set I18NSPHINXOPTS=-D latex_paper_size=%PAPER% %I18NSPHINXOPTS% 14 | ) 15 | 16 | if "%1" == "" goto help 17 | 18 | if "%1" == "help" ( 19 | :help 20 | echo.Please use `make ^` where ^ is one of 21 | echo. html to make standalone HTML files 22 | echo. dirhtml to make HTML files named index.html in directories 23 | echo. singlehtml to make a single large HTML file 24 | echo. pickle to make pickle files 25 | echo. json to make JSON files 26 | echo. htmlhelp to make HTML files and a HTML help project 27 | echo. qthelp to make HTML files and a qthelp project 28 | echo. devhelp to make HTML files and a Devhelp project 29 | echo. epub to make an epub 30 | echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter 31 | echo. text to make text files 32 | echo. man to make manual pages 33 | echo. texinfo to make Texinfo files 34 | echo. gettext to make PO message catalogs 35 | echo. changes to make an overview over all changed/added/deprecated items 36 | echo. xml to make Docutils-native XML files 37 | echo. pseudoxml to make pseudoxml-XML files for display purposes 38 | echo. linkcheck to check all external links for integrity 39 | echo. doctest to run all doctests embedded in the documentation if enabled 40 | echo. coverage to run coverage check of the documentation if enabled 41 | goto end 42 | ) 43 | 44 | if "%1" == "clean" ( 45 | for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i 46 | del /q /s %BUILDDIR%\* 47 | goto end 48 | ) 49 | 50 | 51 | REM Check if sphinx-build is available and fallback to Python version if any 52 | %SPHINXBUILD% 2> nul 53 | if errorlevel 9009 goto sphinx_python 54 | goto sphinx_ok 55 | 56 | :sphinx_python 57 | 58 | set SPHINXBUILD=python -m sphinx.__init__ 59 | %SPHINXBUILD% 2> nul 60 | if errorlevel 9009 ( 61 | echo. 62 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 63 | echo.installed, then set the SPHINXBUILD environment variable to point 64 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 65 | echo.may add the Sphinx directory to PATH. 66 | echo. 67 | echo.If you don't have Sphinx installed, grab it from 68 | echo.http://sphinx-doc.org/ 69 | exit /b 1 70 | ) 71 | 72 | :sphinx_ok 73 | 74 | 75 | if "%1" == "html" ( 76 | %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html 77 | if errorlevel 1 exit /b 1 78 | echo. 79 | echo.Build finished. The HTML pages are in %BUILDDIR%/html. 80 | goto end 81 | ) 82 | 83 | if "%1" == "dirhtml" ( 84 | %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml 85 | if errorlevel 1 exit /b 1 86 | echo. 87 | echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml. 88 | goto end 89 | ) 90 | 91 | if "%1" == "singlehtml" ( 92 | %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml 93 | if errorlevel 1 exit /b 1 94 | echo. 95 | echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml. 96 | goto end 97 | ) 98 | 99 | if "%1" == "pickle" ( 100 | %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle 101 | if errorlevel 1 exit /b 1 102 | echo. 103 | echo.Build finished; now you can process the pickle files. 104 | goto end 105 | ) 106 | 107 | if "%1" == "json" ( 108 | %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json 109 | if errorlevel 1 exit /b 1 110 | echo. 111 | echo.Build finished; now you can process the JSON files. 112 | goto end 113 | ) 114 | 115 | if "%1" == "htmlhelp" ( 116 | %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp 117 | if errorlevel 1 exit /b 1 118 | echo. 119 | echo.Build finished; now you can run HTML Help Workshop with the ^ 120 | .hhp project file in %BUILDDIR%/htmlhelp. 121 | goto end 122 | ) 123 | 124 | if "%1" == "qthelp" ( 125 | %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp 126 | if errorlevel 1 exit /b 1 127 | echo. 128 | echo.Build finished; now you can run "qcollectiongenerator" with the ^ 129 | .qhcp project file in %BUILDDIR%/qthelp, like this: 130 | echo.^> qcollectiongenerator %BUILDDIR%\qthelp\acme-python.qhcp 131 | echo.To view the help file: 132 | echo.^> assistant -collectionFile %BUILDDIR%\qthelp\acme-python.ghc 133 | goto end 134 | ) 135 | 136 | if "%1" == "devhelp" ( 137 | %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp 138 | if errorlevel 1 exit /b 1 139 | echo. 140 | echo.Build finished. 141 | goto end 142 | ) 143 | 144 | if "%1" == "epub" ( 145 | %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub 146 | if errorlevel 1 exit /b 1 147 | echo. 148 | echo.Build finished. The epub file is in %BUILDDIR%/epub. 149 | goto end 150 | ) 151 | 152 | if "%1" == "latex" ( 153 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 154 | if errorlevel 1 exit /b 1 155 | echo. 156 | echo.Build finished; the LaTeX files are in %BUILDDIR%/latex. 157 | goto end 158 | ) 159 | 160 | if "%1" == "latexpdf" ( 161 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 162 | cd %BUILDDIR%/latex 163 | make all-pdf 164 | cd %~dp0 165 | echo. 166 | echo.Build finished; the PDF files are in %BUILDDIR%/latex. 167 | goto end 168 | ) 169 | 170 | if "%1" == "latexpdfja" ( 171 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 172 | cd %BUILDDIR%/latex 173 | make all-pdf-ja 174 | cd %~dp0 175 | echo. 176 | echo.Build finished; the PDF files are in %BUILDDIR%/latex. 177 | goto end 178 | ) 179 | 180 | if "%1" == "text" ( 181 | %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text 182 | if errorlevel 1 exit /b 1 183 | echo. 184 | echo.Build finished. The text files are in %BUILDDIR%/text. 185 | goto end 186 | ) 187 | 188 | if "%1" == "man" ( 189 | %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man 190 | if errorlevel 1 exit /b 1 191 | echo. 192 | echo.Build finished. The manual pages are in %BUILDDIR%/man. 193 | goto end 194 | ) 195 | 196 | if "%1" == "texinfo" ( 197 | %SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo 198 | if errorlevel 1 exit /b 1 199 | echo. 200 | echo.Build finished. The Texinfo files are in %BUILDDIR%/texinfo. 201 | goto end 202 | ) 203 | 204 | if "%1" == "gettext" ( 205 | %SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/locale 206 | if errorlevel 1 exit /b 1 207 | echo. 208 | echo.Build finished. The message catalogs are in %BUILDDIR%/locale. 209 | goto end 210 | ) 211 | 212 | if "%1" == "changes" ( 213 | %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes 214 | if errorlevel 1 exit /b 1 215 | echo. 216 | echo.The overview file is in %BUILDDIR%/changes. 217 | goto end 218 | ) 219 | 220 | if "%1" == "linkcheck" ( 221 | %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck 222 | if errorlevel 1 exit /b 1 223 | echo. 224 | echo.Link check complete; look for any errors in the above output ^ 225 | or in %BUILDDIR%/linkcheck/output.txt. 226 | goto end 227 | ) 228 | 229 | if "%1" == "doctest" ( 230 | %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest 231 | if errorlevel 1 exit /b 1 232 | echo. 233 | echo.Testing of doctests in the sources finished, look at the ^ 234 | results in %BUILDDIR%/doctest/output.txt. 235 | goto end 236 | ) 237 | 238 | if "%1" == "coverage" ( 239 | %SPHINXBUILD% -b coverage %ALLSPHINXOPTS% %BUILDDIR%/coverage 240 | if errorlevel 1 exit /b 1 241 | echo. 242 | echo.Testing of coverage in the sources finished, look at the ^ 243 | results in %BUILDDIR%/coverage/python.txt. 244 | goto end 245 | ) 246 | 247 | if "%1" == "xml" ( 248 | %SPHINXBUILD% -b xml %ALLSPHINXOPTS% %BUILDDIR%/xml 249 | if errorlevel 1 exit /b 1 250 | echo. 251 | echo.Build finished. The XML files are in %BUILDDIR%/xml. 252 | goto end 253 | ) 254 | 255 | if "%1" == "pseudoxml" ( 256 | %SPHINXBUILD% -b pseudoxml %ALLSPHINXOPTS% %BUILDDIR%/pseudoxml 257 | if errorlevel 1 exit /b 1 258 | echo. 259 | echo.Build finished. The pseudo-XML files are in %BUILDDIR%/pseudoxml. 260 | goto end 261 | ) 262 | 263 | :end 264 | -------------------------------------------------------------------------------- /acme/standalone.py: -------------------------------------------------------------------------------- 1 | """Support for standalone client challenge solvers. """ 2 | import collections 3 | import functools 4 | import logging 5 | import socket 6 | import threading 7 | 8 | from six.moves import BaseHTTPServer # type: ignore # pylint: disable=import-error 9 | from six.moves import http_client # pylint: disable=import-error 10 | from six.moves import socketserver # type: ignore # pylint: disable=import-error 11 | 12 | from acme import challenges 13 | from acme import crypto_util 14 | from acme.magic_typing import List # pylint: disable=unused-import, no-name-in-module 15 | 16 | logger = logging.getLogger(__name__) 17 | 18 | # six.moves.* | pylint: disable=no-member,attribute-defined-outside-init 19 | # pylint: disable=no-init 20 | 21 | 22 | class TLSServer(socketserver.TCPServer): 23 | """Generic TLS Server.""" 24 | 25 | def __init__(self, *args, **kwargs): 26 | self.ipv6 = kwargs.pop("ipv6", False) 27 | if self.ipv6: 28 | self.address_family = socket.AF_INET6 29 | else: 30 | self.address_family = socket.AF_INET 31 | self.certs = kwargs.pop("certs", {}) 32 | self.method = kwargs.pop( 33 | # pylint: disable=protected-access 34 | "method", crypto_util._DEFAULT_SSL_METHOD) 35 | self.allow_reuse_address = kwargs.pop("allow_reuse_address", True) 36 | socketserver.TCPServer.__init__(self, *args, **kwargs) 37 | 38 | def _wrap_sock(self): 39 | self.socket = crypto_util.SSLSocket( 40 | self.socket, certs=self.certs, method=self.method) 41 | 42 | def server_bind(self): # pylint: disable=missing-docstring 43 | self._wrap_sock() 44 | return socketserver.TCPServer.server_bind(self) 45 | 46 | 47 | class ACMEServerMixin: 48 | """ACME server common settings mixin.""" 49 | # TODO: c.f. #858 50 | server_version = "ACME client standalone challenge solver" 51 | allow_reuse_address = True 52 | 53 | 54 | class BaseDualNetworkedServers(object): 55 | """Base class for a pair of IPv6 and IPv4 servers that tries to do everything 56 | it's asked for both servers, but where failures in one server don't 57 | affect the other. 58 | 59 | If two servers are instantiated, they will serve on the same port. 60 | """ 61 | 62 | def __init__(self, ServerClass, server_address, *remaining_args, **kwargs): 63 | port = server_address[1] 64 | self.threads = [] # type: List[threading.Thread] 65 | self.servers = [] # type: List[ACMEServerMixin] 66 | 67 | # Must try True first. 68 | # Ubuntu, for example, will fail to bind to IPv4 if we've already bound 69 | # to IPv6. But that's ok, since it will accept IPv4 connections on the IPv6 70 | # socket. On the other hand, FreeBSD will successfully bind to IPv4 on the 71 | # same port, which means that server will accept the IPv4 connections. 72 | # If Python is compiled without IPv6, we'll error out but (probably) successfully 73 | # create the IPv4 server. 74 | for ip_version in [True, False]: 75 | try: 76 | kwargs["ipv6"] = ip_version 77 | new_address = (server_address[0],) + (port,) + server_address[2:] 78 | new_args = (new_address,) + remaining_args 79 | server = ServerClass(*new_args, **kwargs) 80 | logger.debug( 81 | "Successfully bound to %s:%s using %s", new_address[0], 82 | new_address[1], "IPv6" if ip_version else "IPv4") 83 | except socket.error: 84 | if self.servers: 85 | # Already bound using IPv6. 86 | logger.debug( 87 | "Certbot wasn't able to bind to %s:%s using %s, this " 88 | "is often expected due to the dual stack nature of " 89 | "IPv6 socket implementations.", 90 | new_address[0], new_address[1], 91 | "IPv6" if ip_version else "IPv4") 92 | else: 93 | logger.debug( 94 | "Failed to bind to %s:%s using %s", new_address[0], 95 | new_address[1], "IPv6" if ip_version else "IPv4") 96 | else: 97 | self.servers.append(server) 98 | # If two servers are set up and port 0 was passed in, ensure we always 99 | # bind to the same port for both servers. 100 | port = server.socket.getsockname()[1] 101 | if not self.servers: 102 | raise socket.error("Could not bind to IPv4 or IPv6.") 103 | 104 | def serve_forever(self): 105 | """Wraps socketserver.TCPServer.serve_forever""" 106 | for server in self.servers: 107 | thread = threading.Thread( 108 | target=server.serve_forever) 109 | thread.start() 110 | self.threads.append(thread) 111 | 112 | def getsocknames(self): 113 | """Wraps socketserver.TCPServer.socket.getsockname""" 114 | return [server.socket.getsockname() for server in self.servers] 115 | 116 | def shutdown_and_server_close(self): 117 | """Wraps socketserver.TCPServer.shutdown, socketserver.TCPServer.server_close, and 118 | threading.Thread.join""" 119 | for server in self.servers: 120 | server.shutdown() 121 | server.server_close() 122 | for thread in self.threads: 123 | thread.join() 124 | self.threads = [] 125 | 126 | 127 | class HTTPServer(BaseHTTPServer.HTTPServer): 128 | """Generic HTTP Server.""" 129 | 130 | def __init__(self, *args, **kwargs): 131 | self.ipv6 = kwargs.pop("ipv6", False) 132 | if self.ipv6: 133 | self.address_family = socket.AF_INET6 134 | else: 135 | self.address_family = socket.AF_INET 136 | BaseHTTPServer.HTTPServer.__init__(self, *args, **kwargs) 137 | 138 | 139 | class HTTP01Server(HTTPServer, ACMEServerMixin): 140 | """HTTP01 Server.""" 141 | 142 | def __init__(self, server_address, resources, ipv6=False): 143 | HTTPServer.__init__( 144 | self, server_address, HTTP01RequestHandler.partial_init( 145 | simple_http_resources=resources), ipv6=ipv6) 146 | 147 | 148 | class HTTP01DualNetworkedServers(BaseDualNetworkedServers): 149 | """HTTP01Server Wrapper. Tries everything for both. Failures for one don't 150 | affect the other.""" 151 | 152 | def __init__(self, *args, **kwargs): 153 | BaseDualNetworkedServers.__init__(self, HTTP01Server, *args, **kwargs) 154 | 155 | 156 | class HTTP01RequestHandler(BaseHTTPServer.BaseHTTPRequestHandler): 157 | """HTTP01 challenge handler. 158 | 159 | Adheres to the stdlib's `socketserver.BaseRequestHandler` interface. 160 | 161 | :ivar set simple_http_resources: A set of `HTTP01Resource` 162 | objects. TODO: better name? 163 | 164 | """ 165 | HTTP01Resource = collections.namedtuple( 166 | "HTTP01Resource", "chall response validation") 167 | 168 | def __init__(self, *args, **kwargs): 169 | self.simple_http_resources = kwargs.pop("simple_http_resources", set()) 170 | BaseHTTPServer.BaseHTTPRequestHandler.__init__(self, *args, **kwargs) 171 | 172 | def log_message(self, format, *args): # pylint: disable=redefined-builtin 173 | """Log arbitrary message.""" 174 | logger.debug("%s - - %s", self.client_address[0], format % args) 175 | 176 | def handle(self): 177 | """Handle request.""" 178 | self.log_message("Incoming request") 179 | BaseHTTPServer.BaseHTTPRequestHandler.handle(self) 180 | 181 | def do_GET(self): # pylint: disable=invalid-name,missing-docstring 182 | if self.path == "/": 183 | self.handle_index() 184 | elif self.path.startswith("/" + challenges.HTTP01.URI_ROOT_PATH): 185 | self.handle_simple_http_resource() 186 | else: 187 | self.handle_404() 188 | 189 | def handle_index(self): 190 | """Handle index page.""" 191 | self.send_response(200) 192 | self.send_header("Content-Type", "text/html") 193 | self.end_headers() 194 | self.wfile.write(self.server.server_version.encode()) 195 | 196 | def handle_404(self): 197 | """Handler 404 Not Found errors.""" 198 | self.send_response(http_client.NOT_FOUND, message="Not Found") 199 | self.send_header("Content-type", "text/html") 200 | self.end_headers() 201 | self.wfile.write(b"404") 202 | 203 | def handle_simple_http_resource(self): 204 | """Handle HTTP01 provisioned resources.""" 205 | for resource in self.simple_http_resources: 206 | if resource.chall.path == self.path: 207 | self.log_message("Serving HTTP01 with token %r", 208 | resource.chall.encode("token")) 209 | self.send_response(http_client.OK) 210 | self.end_headers() 211 | self.wfile.write(resource.validation.encode()) 212 | return 213 | else: # pylint: disable=useless-else-on-loop 214 | self.log_message("No resources to serve") 215 | self.log_message("%s does not correspond to any resource. ignoring", 216 | self.path) 217 | 218 | @classmethod 219 | def partial_init(cls, simple_http_resources): 220 | """Partially initialize this handler. 221 | 222 | This is useful because `socketserver.BaseServer` takes 223 | uninitialized handler and initializes it with the current 224 | request. 225 | 226 | """ 227 | return functools.partial( 228 | cls, simple_http_resources=simple_http_resources) 229 | -------------------------------------------------------------------------------- /debian/changelog: -------------------------------------------------------------------------------- 1 | python-acme (1.1.0-1) unstable; urgency=medium 2 | 3 | * New upstream version 1.1.0 4 | * Bump S-V; no changes needed. 5 | * Drop inactive Uploaders 6 | * Drop versioned depends older than oldstable (cme fix) 7 | 8 | -- Harlan Lieberman-Berg Thu, 23 Jan 2020 22:53:17 -0500 9 | 10 | python-acme (0.40.0-1) unstable; urgency=medium 11 | 12 | * New upstream version 0.40.0 13 | * Switch to debhelper-compat instead of d/compat 14 | * Bump S-V; no changes needed. 15 | 16 | -- Harlan Lieberman-Berg Tue, 05 Nov 2019 19:28:06 -0500 17 | 18 | python-acme (0.39.0-1) unstable; urgency=medium 19 | 20 | * New upstream version 0.39.0 21 | * Drop Python 2 packages. (Closes: #935211) 22 | * Move docs path to python-acme-doc 23 | 24 | -- Harlan Lieberman-Berg Tue, 01 Oct 2019 21:40:04 -0400 25 | 26 | python-acme (0.36.0-1) unstable; urgency=medium 27 | 28 | * New upstream version 0.36.0 29 | * Bump compat to 12. 30 | * Bump S-V; no changes needed. 31 | 32 | -- Harlan Lieberman-Berg Thu, 11 Jul 2019 16:28:33 -0400 33 | 34 | python-acme (0.35.1-1) unstable; urgency=medium 35 | 36 | * New upstream version 0.35.1. 37 | 38 | -- Harlan Lieberman-Berg Sun, 07 Jul 2019 20:43:56 -0400 39 | 40 | python-acme (0.31.0-1) unstable; urgency=medium 41 | 42 | * Bump dependency on josepy to >= 1.1.0 43 | * Add Breaks on python-acme against certbot << 0.20 44 | * New upstream version 0.31.0 45 | * Add dep on python-idna required by security extra. 46 | * Bump S-V; no changes needed. 47 | 48 | -- Harlan Lieberman-Berg Sat, 09 Feb 2019 19:07:59 -0500 49 | 50 | python-acme (0.28.0-1) unstable; urgency=medium 51 | 52 | * New upstream version 0.28.0 53 | 54 | -- Harlan Lieberman-Berg Wed, 07 Nov 2018 18:05:59 -0500 55 | 56 | python-acme (0.27.0-1) unstable; urgency=medium 57 | 58 | * New upstream release. 59 | * Bump S-V; no changes needed. 60 | 61 | -- Harlan Lieberman-Berg Wed, 05 Sep 2018 20:12:58 -0400 62 | 63 | python-acme (0.26.0-1) unstable; urgency=medium 64 | 65 | * New upstream version 0.26.0 66 | * Bump S-V; add Rules-Require-Root: no 67 | 68 | -- Harlan Lieberman-Berg Thu, 12 Jul 2018 22:07:01 -0400 69 | 70 | python-acme (0.25.1-1) unstable; urgency=medium 71 | 72 | * New upstream version 0.25.1 73 | 74 | -- Harlan Lieberman-Berg Wed, 13 Jun 2018 22:28:55 -0400 75 | 76 | python-acme (0.25.0-1) unstable; urgency=medium 77 | 78 | * New upstream version 0.25.0 79 | * Add new dependency on requests-toolbelt 80 | * Drop unnecessary X-Python-Version fields 81 | * Add pytest as build-time dep only. 82 | 83 | -- Harlan Lieberman-Berg Mon, 11 Jun 2018 21:54:41 -0400 84 | 85 | python-acme (0.24.0-2) unstable; urgency=medium 86 | 87 | * Update team email address. (Closes: #895863) 88 | 89 | -- Harlan Lieberman-Berg Fri, 04 May 2018 20:33:30 -0400 90 | 91 | python-acme (0.24.0-1) unstable; urgency=medium 92 | 93 | * New upstream release. 94 | * Bump S-V; no changes needed. 95 | 96 | -- Harlan Lieberman-Berg Thu, 03 May 2018 19:30:10 -0400 97 | 98 | python-acme (0.22.2-1) unstable; urgency=medium 99 | 100 | * New upstream release. 101 | 102 | -- Harlan Lieberman-Berg Wed, 21 Mar 2018 00:45:24 -0400 103 | 104 | python-acme (0.22.0-1) unstable; urgency=medium 105 | 106 | * New upstream release -- now with wildcards! 107 | 108 | -- Harlan Lieberman-Berg Thu, 15 Mar 2018 20:11:05 -0400 109 | 110 | python-acme (0.21.1-1) unstable; urgency=high 111 | 112 | * New upstream release. 113 | * Cleanup from josepy separation. 114 | 115 | -- Harlan Lieberman-Berg Tue, 30 Jan 2018 19:25:01 -0500 116 | 117 | python-acme (0.20.0-1) unstable; urgency=low 118 | 119 | * New upstream release. 120 | * Add new dependencies introduced upstream. 121 | * Bump S-V, debhelper versions. 122 | * Move doc-base ref to package instead of package-doc. 123 | 124 | -- Harlan Lieberman-Berg Fri, 05 Jan 2018 21:44:42 -0500 125 | 126 | python-acme (0.19.0-1) unstable; urgency=medium 127 | 128 | * New upstream release. 129 | 130 | -- Harlan Lieberman-Berg Wed, 04 Oct 2017 19:32:09 -0400 131 | 132 | python-acme (0.18.2-1) unstable; urgency=medium 133 | 134 | * New upstream release. 135 | * Bump S-V; no changes needed. 136 | * Switch to python3-sphinx for docs. 137 | 138 | -- Harlan Lieberman-Berg Sun, 01 Oct 2017 17:38:25 -0400 139 | 140 | python-acme (0.17.0-1) unstable; urgency=medium 141 | 142 | * New upstream release. 143 | * Reduce dependency on python-requests, following upstream. 144 | * Increase priority to optional to comply with Policy v4.0.1.0 145 | * Declare Testsuite using simple autopkgtest. 146 | * Bump S-V to 4.0.1. 147 | 148 | -- Harlan Lieberman-Berg Sun, 06 Aug 2017 14:11:53 -0400 149 | 150 | python-acme (0.14.2-1) experimental; urgency=medium 151 | 152 | * Team upload. 153 | * New upstream release. 154 | 155 | -- Robie Basak Fri, 26 May 2017 12:41:31 +0100 156 | 157 | python-acme (0.12.0-1) experimental; urgency=medium 158 | 159 | * New upstream release. 160 | 161 | -- Harlan Lieberman-Berg Sat, 18 Mar 2017 18:13:01 -0400 162 | 163 | python-acme (0.11.1-1) unstable; urgency=medium 164 | 165 | * New upstream release. 166 | * Drop dep on python3?-dnspython removed upstream 167 | 168 | -- Harlan Lieberman-Berg Thu, 02 Feb 2017 20:31:00 -0500 169 | 170 | python-acme (0.10.2-1) unstable; urgency=medium 171 | 172 | * New upstream release. 173 | 174 | -- Harlan Lieberman-Berg Thu, 26 Jan 2017 01:06:21 -0500 175 | 176 | python-acme (0.10.1-1) unstable; urgency=medium 177 | 178 | * New upstream version. 179 | * Drop patch applied upstream. 180 | * Rejigger dependencies. 181 | 182 | -- Harlan Lieberman-Berg Thu, 19 Jan 2017 23:37:33 -0500 183 | 184 | python-acme (0.9.3-2) unstable; urgency=medium 185 | 186 | * Add patch to fix tests with OpenSSL 1.1 (Closes: #844944) 187 | 188 | -- Harlan Lieberman-Berg Fri, 02 Dec 2016 17:53:38 -0500 189 | 190 | python-acme (0.9.3-1) unstable; urgency=medium 191 | 192 | * New upstream release. 193 | 194 | -- Harlan Lieberman-Berg Thu, 13 Oct 2016 22:21:10 -0400 195 | 196 | python-acme (0.8.1-2) unstable; urgency=medium 197 | 198 | * Ensure there's no network access at build time. 199 | 200 | -- Harlan Lieberman-Berg Fri, 02 Sep 2016 17:47:46 -0400 201 | 202 | python-acme (0.8.1-1) unstable; urgency=medium 203 | 204 | * New upstream release. 205 | * Specify python-setuptools version. (Closes: #825619) 206 | 207 | -- Harlan Lieberman-Berg Thu, 30 Jun 2016 18:54:04 -0400 208 | 209 | python-acme (0.8.0-1) unstable; urgency=high 210 | 211 | * New upstream release. 212 | * Add version dep on python-setuptools. 213 | * Add Suggests for -doc. 214 | 215 | -- Harlan Lieberman-Berg Thu, 02 Jun 2016 19:22:18 -0400 216 | 217 | python-acme (0.6.0-1) unstable; urgency=medium 218 | 219 | * New upstream release. 220 | * Add Breaks on python-certbot in rename prep. 221 | * Bump S-V; no changes needed. 222 | 223 | -- Harlan Lieberman-Berg Thu, 12 May 2016 18:38:11 -0400 224 | 225 | python-acme (0.5.0-1) unstable; urgency=medium 226 | 227 | * New upstream release. 228 | 229 | -- Harlan Lieberman-Berg Thu, 07 Apr 2016 21:56:05 -0400 230 | 231 | python-acme (0.4.1-1) unstable; urgency=medium 232 | 233 | * New upstream release. 234 | * Drop B-D on python3?-werkezug no longer needed by upstream 235 | * Fix Vcs-git URL 236 | * Bump S-V; no changes needed 237 | 238 | -- Harlan Lieberman-Berg Mon, 29 Feb 2016 21:09:41 -0500 239 | 240 | python-acme (0.4.0-1) unstable; urgency=medium 241 | 242 | * New upstream release. 243 | 244 | -- Harlan Lieberman-Berg Thu, 11 Feb 2016 19:39:40 -0500 245 | 246 | python-acme (0.3.0-1) unstable; urgency=medium 247 | 248 | * New upstream release. 249 | * Switch Vcs-git to https. 250 | 251 | -- Harlan Lieberman-Berg Thu, 28 Jan 2016 18:55:06 -0500 252 | 253 | python-acme (0.2.0-1) unstable; urgency=medium 254 | 255 | * New upstream release. 256 | * Drop priority to extra, due to dep on python-mock. 257 | 258 | -- Harlan Lieberman-Berg Fri, 15 Jan 2016 19:04:18 -0500 259 | 260 | python-acme (0.1.1-1) unstable; urgency=medium 261 | 262 | * New upstream release. 263 | 264 | -- Harlan Lieberman-Berg Tue, 15 Dec 2015 21:32:13 -0500 265 | 266 | python-acme (0.1.0-3) unstable; urgency=medium 267 | 268 | * Bump for release to sid. 269 | 270 | -- Harlan Lieberman-Berg Thu, 10 Dec 2015 23:42:46 -0500 271 | 272 | python-acme (0.1.0-2) experimental; urgency=medium 273 | 274 | * Version the python{,3}-openssl runtime dependency (closes: #807114) 275 | 276 | -- Francois Marier Sat, 05 Dec 2015 21:21:10 -0800 277 | 278 | python-acme (0.1.0-1) experimental; urgency=medium 279 | 280 | * New upstream release. 281 | * Change to using new GPG key. 282 | 283 | -- Harlan Lieberman-Berg Thu, 03 Dec 2015 21:13:28 -0500 284 | 285 | python-acme (0.0.0.dev20151123-2) experimental; urgency=medium 286 | 287 | * Add Breaks directive to help apt figure out upgrade paths. 288 | 289 | -- Harlan Lieberman-Berg Tue, 01 Dec 2015 21:15:49 -0500 290 | 291 | python-acme (0.0.0.dev20151123-1) experimental; urgency=medium 292 | 293 | * New upstream version. 294 | 295 | -- Harlan Lieberman-Berg Sun, 29 Nov 2015 23:22:02 -0500 296 | 297 | python-acme (0.0.0.dev20151114-1) experimental; urgency=medium 298 | 299 | [ Francois Marier ] 300 | * Add Vcs fields to debian/control 301 | * Bump the python-sphinx dependency to >= 1.3.1-1 to ensure that 302 | python-sphinx-rtd-theme is pulled in. 303 | 304 | [ Harlan Lieberman-Berg ] 305 | * New upstream release. 306 | * Alter python-sphinx dependency for backport compatibility. 307 | * Update dependency versions to match setup.py. 308 | 309 | -- Harlan Lieberman-Berg Sat, 14 Nov 2015 12:07:50 -0500 310 | 311 | python-acme (0.0.0.dev20151104-1) experimental; urgency=medium 312 | 313 | * Initial release. (Closes: #801356) 314 | 315 | -- Francois Marier Wed, 11 Nov 2015 17:19:23 -0800 316 | -------------------------------------------------------------------------------- /tests/crypto_util_test.py: -------------------------------------------------------------------------------- 1 | """Tests for acme.crypto_util.""" 2 | import itertools 3 | import socket 4 | import threading 5 | import time 6 | import unittest 7 | 8 | import josepy as jose 9 | import OpenSSL 10 | import six 11 | from six.moves import socketserver # type: ignore # pylint: disable=import-error 12 | 13 | from acme import errors 14 | from acme.magic_typing import List # pylint: disable=unused-import, no-name-in-module 15 | import test_util 16 | 17 | 18 | class SSLSocketAndProbeSNITest(unittest.TestCase): 19 | """Tests for acme.crypto_util.SSLSocket/probe_sni.""" 20 | 21 | 22 | def setUp(self): 23 | self.cert = test_util.load_comparable_cert('rsa2048_cert.pem') 24 | key = test_util.load_pyopenssl_private_key('rsa2048_key.pem') 25 | # pylint: disable=protected-access 26 | certs = {b'foo': (key, self.cert.wrapped)} 27 | 28 | from acme.crypto_util import SSLSocket 29 | 30 | class _TestServer(socketserver.TCPServer): 31 | 32 | # six.moves.* | pylint: disable=attribute-defined-outside-init,no-init 33 | 34 | def server_bind(self): # pylint: disable=missing-docstring 35 | self.socket = SSLSocket(socket.socket(), certs=certs) 36 | socketserver.TCPServer.server_bind(self) 37 | 38 | self.server = _TestServer(('', 0), socketserver.BaseRequestHandler) 39 | self.port = self.server.socket.getsockname()[1] 40 | self.server_thread = threading.Thread( 41 | target=self.server.handle_request) 42 | 43 | def tearDown(self): 44 | if self.server_thread.is_alive(): 45 | # The thread may have already terminated. 46 | self.server_thread.join() # pragma: no cover 47 | 48 | def _probe(self, name): 49 | from acme.crypto_util import probe_sni 50 | return jose.ComparableX509(probe_sni( 51 | name, host='127.0.0.1', port=self.port)) 52 | 53 | def _start_server(self): 54 | self.server_thread.start() 55 | time.sleep(1) # TODO: avoid race conditions in other way 56 | 57 | def test_probe_ok(self): 58 | self._start_server() 59 | self.assertEqual(self.cert, self._probe(b'foo')) 60 | 61 | def test_probe_not_recognized_name(self): 62 | self._start_server() 63 | self.assertRaises(errors.Error, self._probe, b'bar') 64 | 65 | def test_probe_connection_error(self): 66 | # pylint has a hard time with six 67 | self.server.server_close() 68 | original_timeout = socket.getdefaulttimeout() 69 | try: 70 | socket.setdefaulttimeout(1) 71 | self.assertRaises(errors.Error, self._probe, b'bar') 72 | finally: 73 | socket.setdefaulttimeout(original_timeout) 74 | 75 | 76 | class PyOpenSSLCertOrReqAllNamesTest(unittest.TestCase): 77 | """Test for acme.crypto_util._pyopenssl_cert_or_req_all_names.""" 78 | 79 | @classmethod 80 | def _call(cls, loader, name): 81 | # pylint: disable=protected-access 82 | from acme.crypto_util import _pyopenssl_cert_or_req_all_names 83 | return _pyopenssl_cert_or_req_all_names(loader(name)) 84 | 85 | def _call_cert(self, name): 86 | return self._call(test_util.load_cert, name) 87 | 88 | def test_cert_one_san_no_common(self): 89 | self.assertEqual(self._call_cert('cert-nocn.der'), 90 | ['no-common-name.badssl.com']) 91 | 92 | def test_cert_no_sans_yes_common(self): 93 | self.assertEqual(self._call_cert('cert.pem'), ['example.com']) 94 | 95 | def test_cert_two_sans_yes_common(self): 96 | self.assertEqual(self._call_cert('cert-san.pem'), 97 | ['example.com', 'www.example.com']) 98 | 99 | 100 | class PyOpenSSLCertOrReqSANTest(unittest.TestCase): 101 | """Test for acme.crypto_util._pyopenssl_cert_or_req_san.""" 102 | 103 | 104 | @classmethod 105 | def _call(cls, loader, name): 106 | # pylint: disable=protected-access 107 | from acme.crypto_util import _pyopenssl_cert_or_req_san 108 | return _pyopenssl_cert_or_req_san(loader(name)) 109 | 110 | @classmethod 111 | def _get_idn_names(cls): 112 | """Returns expected names from '{cert,csr}-idnsans.pem'.""" 113 | chars = [six.unichr(i) for i in itertools.chain(range(0x3c3, 0x400), 114 | range(0x641, 0x6fc), 115 | range(0x1820, 0x1877))] 116 | return [''.join(chars[i: i + 45]) + '.invalid' 117 | for i in range(0, len(chars), 45)] 118 | 119 | def _call_cert(self, name): 120 | return self._call(test_util.load_cert, name) 121 | 122 | def _call_csr(self, name): 123 | return self._call(test_util.load_csr, name) 124 | 125 | def test_cert_no_sans(self): 126 | self.assertEqual(self._call_cert('cert.pem'), []) 127 | 128 | def test_cert_two_sans(self): 129 | self.assertEqual(self._call_cert('cert-san.pem'), 130 | ['example.com', 'www.example.com']) 131 | 132 | def test_cert_hundred_sans(self): 133 | self.assertEqual(self._call_cert('cert-100sans.pem'), 134 | ['example{0}.com'.format(i) for i in range(1, 101)]) 135 | 136 | def test_cert_idn_sans(self): 137 | self.assertEqual(self._call_cert('cert-idnsans.pem'), 138 | self._get_idn_names()) 139 | 140 | def test_csr_no_sans(self): 141 | self.assertEqual(self._call_csr('csr-nosans.pem'), []) 142 | 143 | def test_csr_one_san(self): 144 | self.assertEqual(self._call_csr('csr.pem'), ['example.com']) 145 | 146 | def test_csr_two_sans(self): 147 | self.assertEqual(self._call_csr('csr-san.pem'), 148 | ['example.com', 'www.example.com']) 149 | 150 | def test_csr_six_sans(self): 151 | self.assertEqual(self._call_csr('csr-6sans.pem'), 152 | ['example.com', 'example.org', 'example.net', 153 | 'example.info', 'subdomain.example.com', 154 | 'other.subdomain.example.com']) 155 | 156 | def test_csr_hundred_sans(self): 157 | self.assertEqual(self._call_csr('csr-100sans.pem'), 158 | ['example{0}.com'.format(i) for i in range(1, 101)]) 159 | 160 | def test_csr_idn_sans(self): 161 | self.assertEqual(self._call_csr('csr-idnsans.pem'), 162 | self._get_idn_names()) 163 | 164 | def test_critical_san(self): 165 | self.assertEqual(self._call_cert('critical-san.pem'), 166 | ['chicago-cubs.venafi.example', 'cubs.venafi.example']) 167 | 168 | 169 | 170 | class RandomSnTest(unittest.TestCase): 171 | """Test for random certificate serial numbers.""" 172 | 173 | 174 | def setUp(self): 175 | self.cert_count = 5 176 | self.serial_num = [] # type: List[int] 177 | self.key = OpenSSL.crypto.PKey() 178 | self.key.generate_key(OpenSSL.crypto.TYPE_RSA, 2048) 179 | 180 | def test_sn_collisions(self): 181 | from acme.crypto_util import gen_ss_cert 182 | 183 | for _ in range(self.cert_count): 184 | cert = gen_ss_cert(self.key, ['dummy'], force_san=True) 185 | self.serial_num.append(cert.get_serial_number()) 186 | self.assertTrue(len(set(self.serial_num)) > 1) 187 | 188 | class MakeCSRTest(unittest.TestCase): 189 | """Test for standalone functions.""" 190 | 191 | @classmethod 192 | def _call_with_key(cls, *args, **kwargs): 193 | privkey = OpenSSL.crypto.PKey() 194 | privkey.generate_key(OpenSSL.crypto.TYPE_RSA, 2048) 195 | privkey_pem = OpenSSL.crypto.dump_privatekey(OpenSSL.crypto.FILETYPE_PEM, privkey) 196 | from acme.crypto_util import make_csr 197 | return make_csr(privkey_pem, *args, **kwargs) 198 | 199 | def test_make_csr(self): 200 | csr_pem = self._call_with_key(["a.example", "b.example"]) 201 | self.assertTrue(b'--BEGIN CERTIFICATE REQUEST--' in csr_pem) 202 | self.assertTrue(b'--END CERTIFICATE REQUEST--' in csr_pem) 203 | csr = OpenSSL.crypto.load_certificate_request( 204 | OpenSSL.crypto.FILETYPE_PEM, csr_pem) 205 | # In pyopenssl 0.13 (used with TOXENV=py27-oldest), csr objects don't 206 | # have a get_extensions() method, so we skip this test if the method 207 | # isn't available. 208 | if hasattr(csr, 'get_extensions'): 209 | self.assertEqual(len(csr.get_extensions()), 1) 210 | self.assertEqual(csr.get_extensions()[0].get_data(), 211 | OpenSSL.crypto.X509Extension( 212 | b'subjectAltName', 213 | critical=False, 214 | value=b'DNS:a.example, DNS:b.example', 215 | ).get_data(), 216 | ) 217 | 218 | def test_make_csr_must_staple(self): 219 | csr_pem = self._call_with_key(["a.example"], must_staple=True) 220 | csr = OpenSSL.crypto.load_certificate_request( 221 | OpenSSL.crypto.FILETYPE_PEM, csr_pem) 222 | 223 | # In pyopenssl 0.13 (used with TOXENV=py27-oldest), csr objects don't 224 | # have a get_extensions() method, so we skip this test if the method 225 | # isn't available. 226 | if hasattr(csr, 'get_extensions'): 227 | self.assertEqual(len(csr.get_extensions()), 2) 228 | # NOTE: Ideally we would filter by the TLS Feature OID, but 229 | # OpenSSL.crypto.X509Extension doesn't give us the extension's raw OID, 230 | # and the shortname field is just "UNDEF" 231 | must_staple_exts = [e for e in csr.get_extensions() 232 | if e.get_data() == b"0\x03\x02\x01\x05"] 233 | self.assertEqual(len(must_staple_exts), 1, 234 | "Expected exactly one Must Staple extension") 235 | 236 | 237 | class DumpPyopensslChainTest(unittest.TestCase): 238 | """Test for dump_pyopenssl_chain.""" 239 | 240 | @classmethod 241 | def _call(cls, loaded): 242 | # pylint: disable=protected-access 243 | from acme.crypto_util import dump_pyopenssl_chain 244 | return dump_pyopenssl_chain(loaded) 245 | 246 | def test_dump_pyopenssl_chain(self): 247 | names = ['cert.pem', 'cert-san.pem', 'cert-idnsans.pem'] 248 | loaded = [test_util.load_cert(name) for name in names] 249 | length = sum( 250 | len(OpenSSL.crypto.dump_certificate(OpenSSL.crypto.FILETYPE_PEM, cert)) 251 | for cert in loaded) 252 | self.assertEqual(len(self._call(loaded)), length) 253 | 254 | def test_dump_pyopenssl_chain_wrapped(self): 255 | names = ['cert.pem', 'cert-san.pem', 'cert-idnsans.pem'] 256 | loaded = [test_util.load_cert(name) for name in names] 257 | wrap_func = jose.ComparableX509 258 | wrapped = [wrap_func(cert) for cert in loaded] 259 | dump_func = OpenSSL.crypto.dump_certificate 260 | length = sum(len(dump_func(OpenSSL.crypto.FILETYPE_PEM, cert)) for cert in loaded) 261 | self.assertEqual(len(self._call(wrapped)), length) 262 | 263 | 264 | if __name__ == '__main__': 265 | unittest.main() # pragma: no cover 266 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright 2015 Electronic Frontier Foundation and others 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. 14 | 15 | Apache License 16 | Version 2.0, January 2004 17 | http://www.apache.org/licenses/ 18 | 19 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 20 | 21 | 1. Definitions. 22 | 23 | "License" shall mean the terms and conditions for use, reproduction, 24 | and distribution as defined by Sections 1 through 9 of this document. 25 | 26 | "Licensor" shall mean the copyright owner or entity authorized by 27 | the copyright owner that is granting the License. 28 | 29 | "Legal Entity" shall mean the union of the acting entity and all 30 | other entities that control, are controlled by, or are under common 31 | control with that entity. For the purposes of this definition, 32 | "control" means (i) the power, direct or indirect, to cause the 33 | direction or management of such entity, whether by contract or 34 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 35 | outstanding shares, or (iii) beneficial ownership of such entity. 36 | 37 | "You" (or "Your") shall mean an individual or Legal Entity 38 | exercising permissions granted by this License. 39 | 40 | "Source" form shall mean the preferred form for making modifications, 41 | including but not limited to software source code, documentation 42 | source, and configuration files. 43 | 44 | "Object" form shall mean any form resulting from mechanical 45 | transformation or translation of a Source form, including but 46 | not limited to compiled object code, generated documentation, 47 | and conversions to other media types. 48 | 49 | "Work" shall mean the work of authorship, whether in Source or 50 | Object form, made available under the License, as indicated by a 51 | copyright notice that is included in or attached to the work 52 | (an example is provided in the Appendix below). 53 | 54 | "Derivative Works" shall mean any work, whether in Source or Object 55 | form, that is based on (or derived from) the Work and for which the 56 | editorial revisions, annotations, elaborations, or other modifications 57 | represent, as a whole, an original work of authorship. For the purposes 58 | of this License, Derivative Works shall not include works that remain 59 | separable from, or merely link (or bind by name) to the interfaces of, 60 | the Work and Derivative Works thereof. 61 | 62 | "Contribution" shall mean any work of authorship, including 63 | the original version of the Work and any modifications or additions 64 | to that Work or Derivative Works thereof, that is intentionally 65 | submitted to Licensor for inclusion in the Work by the copyright owner 66 | or by an individual or Legal Entity authorized to submit on behalf of 67 | the copyright owner. For the purposes of this definition, "submitted" 68 | means any form of electronic, verbal, or written communication sent 69 | to the Licensor or its representatives, including but not limited to 70 | communication on electronic mailing lists, source code control systems, 71 | and issue tracking systems that are managed by, or on behalf of, the 72 | Licensor for the purpose of discussing and improving the Work, but 73 | excluding communication that is conspicuously marked or otherwise 74 | designated in writing by the copyright owner as "Not a Contribution." 75 | 76 | "Contributor" shall mean Licensor and any individual or Legal Entity 77 | on behalf of whom a Contribution has been received by Licensor and 78 | subsequently incorporated within the Work. 79 | 80 | 2. Grant of Copyright License. Subject to the terms and conditions of 81 | this License, each Contributor hereby grants to You a perpetual, 82 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 83 | copyright license to reproduce, prepare Derivative Works of, 84 | publicly display, publicly perform, sublicense, and distribute the 85 | Work and such Derivative Works in Source or Object form. 86 | 87 | 3. Grant of Patent License. Subject to the terms and conditions of 88 | this License, each Contributor hereby grants to You a perpetual, 89 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 90 | (except as stated in this section) patent license to make, have made, 91 | use, offer to sell, sell, import, and otherwise transfer the Work, 92 | where such license applies only to those patent claims licensable 93 | by such Contributor that are necessarily infringed by their 94 | Contribution(s) alone or by combination of their Contribution(s) 95 | with the Work to which such Contribution(s) was submitted. If You 96 | institute patent litigation against any entity (including a 97 | cross-claim or counterclaim in a lawsuit) alleging that the Work 98 | or a Contribution incorporated within the Work constitutes direct 99 | or contributory patent infringement, then any patent licenses 100 | granted to You under this License for that Work shall terminate 101 | as of the date such litigation is filed. 102 | 103 | 4. Redistribution. You may reproduce and distribute copies of the 104 | Work or Derivative Works thereof in any medium, with or without 105 | modifications, and in Source or Object form, provided that You 106 | meet the following conditions: 107 | 108 | (a) You must give any other recipients of the Work or 109 | Derivative Works a copy of this License; and 110 | 111 | (b) You must cause any modified files to carry prominent notices 112 | stating that You changed the files; and 113 | 114 | (c) You must retain, in the Source form of any Derivative Works 115 | that You distribute, all copyright, patent, trademark, and 116 | attribution notices from the Source form of the Work, 117 | excluding those notices that do not pertain to any part of 118 | the Derivative Works; and 119 | 120 | (d) If the Work includes a "NOTICE" text file as part of its 121 | distribution, then any Derivative Works that You distribute must 122 | include a readable copy of the attribution notices contained 123 | within such NOTICE file, excluding those notices that do not 124 | pertain to any part of the Derivative Works, in at least one 125 | of the following places: within a NOTICE text file distributed 126 | as part of the Derivative Works; within the Source form or 127 | documentation, if provided along with the Derivative Works; or, 128 | within a display generated by the Derivative Works, if and 129 | wherever such third-party notices normally appear. The contents 130 | of the NOTICE file are for informational purposes only and 131 | do not modify the License. You may add Your own attribution 132 | notices within Derivative Works that You distribute, alongside 133 | or as an addendum to the NOTICE text from the Work, provided 134 | that such additional attribution notices cannot be construed 135 | as modifying the License. 136 | 137 | You may add Your own copyright statement to Your modifications and 138 | may provide additional or different license terms and conditions 139 | for use, reproduction, or distribution of Your modifications, or 140 | for any such Derivative Works as a whole, provided Your use, 141 | reproduction, and distribution of the Work otherwise complies with 142 | the conditions stated in this License. 143 | 144 | 5. Submission of Contributions. Unless You explicitly state otherwise, 145 | any Contribution intentionally submitted for inclusion in the Work 146 | by You to the Licensor shall be under the terms and conditions of 147 | this License, without any additional terms or conditions. 148 | Notwithstanding the above, nothing herein shall supersede or modify 149 | the terms of any separate license agreement you may have executed 150 | with Licensor regarding such Contributions. 151 | 152 | 6. Trademarks. This License does not grant permission to use the trade 153 | names, trademarks, service marks, or product names of the Licensor, 154 | except as required for reasonable and customary use in describing the 155 | origin of the Work and reproducing the content of the NOTICE file. 156 | 157 | 7. Disclaimer of Warranty. Unless required by applicable law or 158 | agreed to in writing, Licensor provides the Work (and each 159 | Contributor provides its Contributions) on an "AS IS" BASIS, 160 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 161 | implied, including, without limitation, any warranties or conditions 162 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 163 | PARTICULAR PURPOSE. You are solely responsible for determining the 164 | appropriateness of using or redistributing the Work and assume any 165 | risks associated with Your exercise of permissions under this License. 166 | 167 | 8. Limitation of Liability. In no event and under no legal theory, 168 | whether in tort (including negligence), contract, or otherwise, 169 | unless required by applicable law (such as deliberate and grossly 170 | negligent acts) or agreed to in writing, shall any Contributor be 171 | liable to You for damages, including any direct, indirect, special, 172 | incidental, or consequential damages of any character arising as a 173 | result of this License or out of the use or inability to use the 174 | Work (including but not limited to damages for loss of goodwill, 175 | work stoppage, computer failure or malfunction, or any and all 176 | other commercial damages or losses), even if such Contributor 177 | has been advised of the possibility of such damages. 178 | 179 | 9. Accepting Warranty or Additional Liability. While redistributing 180 | the Work or Derivative Works thereof, You may choose to offer, 181 | and charge a fee for, acceptance of support, warranty, indemnity, 182 | or other liability obligations and/or rights consistent with this 183 | License. However, in accepting such obligations, You may act only 184 | on Your own behalf and on Your sole responsibility, not on behalf 185 | of any other Contributor, and only if You agree to indemnify, 186 | defend, and hold each Contributor harmless for any liability 187 | incurred by, or claims asserted against, such Contributor by reason 188 | of your accepting any such warranty or additional liability. 189 | 190 | END OF TERMS AND CONDITIONS 191 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # acme-python documentation build configuration file, created by 4 | # sphinx-quickstart on Sun Oct 18 13:38:06 2015. 5 | # 6 | # This file is execfile()d with the current directory set to its 7 | # containing dir. 8 | # 9 | # Note that not all possible configuration values are present in this 10 | # autogenerated file. 11 | # 12 | # All configuration values have a default; values that are commented out 13 | # serve to show the default. 14 | 15 | import os 16 | import shlex 17 | import sys 18 | 19 | here = os.path.abspath(os.path.dirname(__file__)) 20 | 21 | 22 | # If extensions (or modules to document with autodoc) are in another directory, 23 | # add these directories to sys.path here. If the directory is relative to the 24 | # documentation root, use os.path.abspath to make it absolute, like shown here. 25 | sys.path.insert(0, os.path.abspath(os.path.join(here, '..'))) 26 | 27 | # -- General configuration ------------------------------------------------ 28 | 29 | # If your documentation needs a minimal Sphinx version, state it here. 30 | needs_sphinx = '1.0' 31 | 32 | # Add any Sphinx extension module names here, as strings. They can be 33 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 34 | # ones. 35 | extensions = [ 36 | 'sphinx.ext.autodoc', 37 | 'sphinx.ext.intersphinx', 38 | 'sphinx.ext.todo', 39 | 'sphinx.ext.coverage', 40 | 'sphinx.ext.viewcode', 41 | ] 42 | 43 | autodoc_member_order = 'bysource' 44 | autodoc_default_flags = ['show-inheritance'] 45 | 46 | # Add any paths that contain templates here, relative to this directory. 47 | templates_path = ['_templates'] 48 | 49 | # The suffix(es) of source filenames. 50 | # You can specify multiple suffix as a list of string: 51 | # source_suffix = ['.rst', '.md'] 52 | source_suffix = '.rst' 53 | 54 | # The encoding of source files. 55 | #source_encoding = 'utf-8-sig' 56 | 57 | # The master toctree document. 58 | master_doc = 'index' 59 | 60 | # General information about the project. 61 | project = u'acme-python' 62 | copyright = u'2015-2015, Let\'s Encrypt Project' 63 | author = u'Let\'s Encrypt Project' 64 | 65 | # The version info for the project you're documenting, acts as replacement for 66 | # |version| and |release|, also used in various other places throughout the 67 | # built documents. 68 | # 69 | # The short X.Y version. 70 | version = '0' 71 | # The full version, including alpha/beta/rc tags. 72 | release = '0' 73 | 74 | # The language for content autogenerated by Sphinx. Refer to documentation 75 | # for a list of supported languages. 76 | # 77 | # This is also used if you do content translation via gettext catalogs. 78 | # Usually you set "language" from the command line for these cases. 79 | language = 'en' 80 | 81 | # There are two options for replacing |today|: either, you set today to some 82 | # non-false value, then it is used: 83 | #today = '' 84 | # Else, today_fmt is used as the format for a strftime call. 85 | #today_fmt = '%B %d, %Y' 86 | 87 | # List of patterns, relative to source directory, that match files and 88 | # directories to ignore when looking for source files. 89 | exclude_patterns = ['_build'] 90 | 91 | # The reST default role (used for this markup: `text`) to use for all 92 | # documents. 93 | default_role = 'py:obj' 94 | 95 | # If true, '()' will be appended to :func: etc. cross-reference text. 96 | #add_function_parentheses = True 97 | 98 | # If true, the current module name will be prepended to all description 99 | # unit titles (such as .. function::). 100 | #add_module_names = True 101 | 102 | # If true, sectionauthor and moduleauthor directives will be shown in the 103 | # output. They are ignored by default. 104 | #show_authors = False 105 | 106 | # The name of the Pygments (syntax highlighting) style to use. 107 | pygments_style = 'sphinx' 108 | 109 | # A list of ignored prefixes for module index sorting. 110 | #modindex_common_prefix = [] 111 | 112 | # If true, keep warnings as "system message" paragraphs in the built documents. 113 | #keep_warnings = False 114 | 115 | # If true, `todo` and `todoList` produce output, else they produce nothing. 116 | todo_include_todos = True 117 | 118 | 119 | # -- Options for HTML output ---------------------------------------------- 120 | 121 | # The theme to use for HTML and HTML Help pages. See the documentation for 122 | # a list of builtin themes. 123 | 124 | # http://docs.readthedocs.org/en/latest/theme.html#how-do-i-use-this-locally-and-on-read-the-docs 125 | # on_rtd is whether we are on readthedocs.org 126 | on_rtd = os.environ.get('READTHEDOCS', None) == 'True' 127 | if not on_rtd: # only import and set the theme if we're building docs locally 128 | import sphinx_rtd_theme 129 | html_theme = 'sphinx_rtd_theme' 130 | html_theme_path = [sphinx_rtd_theme.get_html_theme_path()] 131 | # otherwise, readthedocs.org uses their theme by default, so no need to specify it 132 | 133 | # Theme options are theme-specific and customize the look and feel of a theme 134 | # further. For a list of options available for each theme, see the 135 | # documentation. 136 | #html_theme_options = {} 137 | 138 | # Add any paths that contain custom themes here, relative to this directory. 139 | #html_theme_path = [] 140 | 141 | # The name for this set of Sphinx documents. If None, it defaults to 142 | # " v documentation". 143 | #html_title = None 144 | 145 | # A shorter title for the navigation bar. Default is the same as html_title. 146 | #html_short_title = None 147 | 148 | # The name of an image file (relative to this directory) to place at the top 149 | # of the sidebar. 150 | #html_logo = None 151 | 152 | # The name of an image file (within the static path) to use as favicon of the 153 | # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 154 | # pixels large. 155 | #html_favicon = None 156 | 157 | # Add any paths that contain custom static files (such as style sheets) here, 158 | # relative to this directory. They are copied after the builtin static files, 159 | # so a file named "default.css" will overwrite the builtin "default.css". 160 | html_static_path = ['_static'] 161 | 162 | # Add any extra paths that contain custom files (such as robots.txt or 163 | # .htaccess) here, relative to this directory. These files are copied 164 | # directly to the root of the documentation. 165 | #html_extra_path = [] 166 | 167 | # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, 168 | # using the given strftime format. 169 | #html_last_updated_fmt = '%b %d, %Y' 170 | 171 | # If true, SmartyPants will be used to convert quotes and dashes to 172 | # typographically correct entities. 173 | #html_use_smartypants = True 174 | 175 | # Custom sidebar templates, maps document names to template names. 176 | #html_sidebars = {} 177 | 178 | # Additional templates that should be rendered to pages, maps page names to 179 | # template names. 180 | #html_additional_pages = {} 181 | 182 | # If false, no module index is generated. 183 | #html_domain_indices = True 184 | 185 | # If false, no index is generated. 186 | #html_use_index = True 187 | 188 | # If true, the index is split into individual pages for each letter. 189 | #html_split_index = False 190 | 191 | # If true, links to the reST sources are added to the pages. 192 | #html_show_sourcelink = True 193 | 194 | # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. 195 | #html_show_sphinx = True 196 | 197 | # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. 198 | #html_show_copyright = True 199 | 200 | # If true, an OpenSearch description file will be output, and all pages will 201 | # contain a tag referring to it. The value of this option must be the 202 | # base URL from which the finished HTML is served. 203 | #html_use_opensearch = '' 204 | 205 | # This is the file name suffix for HTML files (e.g. ".xhtml"). 206 | #html_file_suffix = None 207 | 208 | # Language to be used for generating the HTML full-text search index. 209 | # Sphinx supports the following languages: 210 | # 'da', 'de', 'en', 'es', 'fi', 'fr', 'hu', 'it', 'ja' 211 | # 'nl', 'no', 'pt', 'ro', 'ru', 'sv', 'tr' 212 | #html_search_language = 'en' 213 | 214 | # A dictionary with options for the search language support, empty by default. 215 | # Now only 'ja' uses this config value 216 | #html_search_options = {'type': 'default'} 217 | 218 | # The name of a javascript file (relative to the configuration directory) that 219 | # implements a search results scorer. If empty, the default will be used. 220 | #html_search_scorer = 'scorer.js' 221 | 222 | # Output file base name for HTML help builder. 223 | htmlhelp_basename = 'acme-pythondoc' 224 | 225 | # -- Options for LaTeX output --------------------------------------------- 226 | 227 | latex_elements = { 228 | # The paper size ('letterpaper' or 'a4paper'). 229 | #'papersize': 'letterpaper', 230 | 231 | # The font size ('10pt', '11pt' or '12pt'). 232 | #'pointsize': '10pt', 233 | 234 | # Additional stuff for the LaTeX preamble. 235 | #'preamble': '', 236 | 237 | # Latex figure (float) alignment 238 | #'figure_align': 'htbp', 239 | } 240 | 241 | # Grouping the document tree into LaTeX files. List of tuples 242 | # (source start file, target name, title, 243 | # author, documentclass [howto, manual, or own class]). 244 | latex_documents = [ 245 | (master_doc, 'acme-python.tex', u'acme-python Documentation', 246 | u'Let\'s Encrypt Project', 'manual'), 247 | ] 248 | 249 | # The name of an image file (relative to this directory) to place at the top of 250 | # the title page. 251 | #latex_logo = None 252 | 253 | # For "manual" documents, if this is true, then toplevel headings are parts, 254 | # not chapters. 255 | #latex_use_parts = False 256 | 257 | # If true, show page references after internal links. 258 | #latex_show_pagerefs = False 259 | 260 | # If true, show URL addresses after external links. 261 | #latex_show_urls = False 262 | 263 | # Documents to append as an appendix to all manuals. 264 | #latex_appendices = [] 265 | 266 | # If false, no module index is generated. 267 | #latex_domain_indices = True 268 | 269 | 270 | # -- Options for manual page output --------------------------------------- 271 | 272 | # One entry per manual page. List of tuples 273 | # (source start file, name, description, authors, manual section). 274 | man_pages = [ 275 | (master_doc, 'acme-python', u'acme-python Documentation', 276 | [author], 1), 277 | ('man/jws', 'jws', u'jws script documentation', [project], 1), 278 | ] 279 | 280 | # If true, show URL addresses after external links. 281 | #man_show_urls = False 282 | 283 | 284 | # -- Options for Texinfo output ------------------------------------------- 285 | 286 | # Grouping the document tree into Texinfo files. List of tuples 287 | # (source start file, target name, title, author, 288 | # dir menu entry, description, category) 289 | texinfo_documents = [ 290 | (master_doc, 'acme-python', u'acme-python Documentation', 291 | author, 'acme-python', 'One line description of project.', 292 | 'Miscellaneous'), 293 | ] 294 | 295 | # Documents to append as an appendix to all manuals. 296 | #texinfo_appendices = [] 297 | 298 | # If false, no module index is generated. 299 | #texinfo_domain_indices = True 300 | 301 | # How to display URL addresses: 'footnote', 'no', or 'inline'. 302 | #texinfo_show_urls = 'footnote' 303 | 304 | # If true, do not generate a @detailmenu in the "Top" node's menu. 305 | #texinfo_no_detailmenu = False 306 | 307 | 308 | intersphinx_mapping = { 309 | 'python': ('https://docs.python.org/', None), 310 | 'josepy': ('https://josepy.readthedocs.io/en/latest/', None), 311 | } 312 | -------------------------------------------------------------------------------- /acme/crypto_util.py: -------------------------------------------------------------------------------- 1 | """Crypto utilities.""" 2 | import binascii 3 | import contextlib 4 | import logging 5 | import os 6 | import re 7 | import socket 8 | 9 | import josepy as jose 10 | from OpenSSL import crypto 11 | from OpenSSL import SSL # type: ignore # https://github.com/python/typeshed/issues/2052 12 | 13 | from acme import errors 14 | from acme.magic_typing import Callable # pylint: disable=unused-import, no-name-in-module 15 | from acme.magic_typing import Optional # pylint: disable=unused-import, no-name-in-module 16 | from acme.magic_typing import Tuple # pylint: disable=unused-import, no-name-in-module 17 | from acme.magic_typing import Union # pylint: disable=unused-import, no-name-in-module 18 | 19 | logger = logging.getLogger(__name__) 20 | 21 | # Default SSL method selected here is the most compatible, while secure 22 | # SSL method: TLSv1_METHOD is only compatible with 23 | # TLSv1_METHOD, while SSLv23_METHOD is compatible with all other 24 | # methods, including TLSv2_METHOD (read more at 25 | # https://www.openssl.org/docs/ssl/SSLv23_method.html). _serve_sni 26 | # should be changed to use "set_options" to disable SSLv2 and SSLv3, 27 | # in case it's used for things other than probing/serving! 28 | _DEFAULT_SSL_METHOD = SSL.SSLv23_METHOD # type: ignore 29 | 30 | 31 | class SSLSocket(object): 32 | """SSL wrapper for sockets. 33 | 34 | :ivar socket sock: Original wrapped socket. 35 | :ivar dict certs: Mapping from domain names (`bytes`) to 36 | `OpenSSL.crypto.X509`. 37 | :ivar method: See `OpenSSL.SSL.Context` for allowed values. 38 | 39 | """ 40 | def __init__(self, sock, certs, method=_DEFAULT_SSL_METHOD): 41 | self.sock = sock 42 | self.certs = certs 43 | self.method = method 44 | 45 | def __getattr__(self, name): 46 | return getattr(self.sock, name) 47 | 48 | def _pick_certificate_cb(self, connection): 49 | """SNI certificate callback. 50 | 51 | This method will set a new OpenSSL context object for this 52 | connection when an incoming connection provides an SNI name 53 | (in order to serve the appropriate certificate, if any). 54 | 55 | :param connection: The TLS connection object on which the SNI 56 | extension was received. 57 | :type connection: :class:`OpenSSL.Connection` 58 | 59 | """ 60 | server_name = connection.get_servername() 61 | try: 62 | key, cert = self.certs[server_name] 63 | except KeyError: 64 | logger.debug("Server name (%s) not recognized, dropping SSL", 65 | server_name) 66 | return 67 | new_context = SSL.Context(self.method) 68 | new_context.set_options(SSL.OP_NO_SSLv2) 69 | new_context.set_options(SSL.OP_NO_SSLv3) 70 | new_context.use_privatekey(key) 71 | new_context.use_certificate(cert) 72 | connection.set_context(new_context) 73 | 74 | class FakeConnection(object): 75 | """Fake OpenSSL.SSL.Connection.""" 76 | 77 | # pylint: disable=missing-docstring 78 | 79 | def __init__(self, connection): 80 | self._wrapped = connection 81 | 82 | def __getattr__(self, name): 83 | return getattr(self._wrapped, name) 84 | 85 | def shutdown(self, *unused_args): 86 | # OpenSSL.SSL.Connection.shutdown doesn't accept any args 87 | return self._wrapped.shutdown() 88 | 89 | def accept(self): # pylint: disable=missing-docstring 90 | sock, addr = self.sock.accept() 91 | 92 | context = SSL.Context(self.method) 93 | context.set_options(SSL.OP_NO_SSLv2) 94 | context.set_options(SSL.OP_NO_SSLv3) 95 | context.set_tlsext_servername_callback(self._pick_certificate_cb) 96 | 97 | ssl_sock = self.FakeConnection(SSL.Connection(context, sock)) 98 | ssl_sock.set_accept_state() 99 | 100 | logger.debug("Performing handshake with %s", addr) 101 | try: 102 | ssl_sock.do_handshake() 103 | except SSL.Error as error: 104 | # _pick_certificate_cb might have returned without 105 | # creating SSL context (wrong server name) 106 | raise socket.error(error) 107 | 108 | return ssl_sock, addr 109 | 110 | 111 | def probe_sni(name, host, port=443, timeout=300, 112 | method=_DEFAULT_SSL_METHOD, source_address=('', 0)): 113 | """Probe SNI server for SSL certificate. 114 | 115 | :param bytes name: Byte string to send as the server name in the 116 | client hello message. 117 | :param bytes host: Host to connect to. 118 | :param int port: Port to connect to. 119 | :param int timeout: Timeout in seconds. 120 | :param method: See `OpenSSL.SSL.Context` for allowed values. 121 | :param tuple source_address: Enables multi-path probing (selection 122 | of source interface). See `socket.creation_connection` for more 123 | info. Available only in Python 2.7+. 124 | 125 | :raises acme.errors.Error: In case of any problems. 126 | 127 | :returns: SSL certificate presented by the server. 128 | :rtype: OpenSSL.crypto.X509 129 | 130 | """ 131 | context = SSL.Context(method) 132 | context.set_timeout(timeout) 133 | 134 | socket_kwargs = {'source_address': source_address} 135 | 136 | try: 137 | logger.debug( 138 | "Attempting to connect to %s:%d%s.", host, port, 139 | " from {0}:{1}".format( 140 | source_address[0], 141 | source_address[1] 142 | ) if socket_kwargs else "" 143 | ) 144 | socket_tuple = (host, port) # type: Tuple[str, int] 145 | sock = socket.create_connection(socket_tuple, **socket_kwargs) # type: ignore 146 | except socket.error as error: 147 | raise errors.Error(error) 148 | 149 | with contextlib.closing(sock) as client: 150 | client_ssl = SSL.Connection(context, client) 151 | client_ssl.set_connect_state() 152 | client_ssl.set_tlsext_host_name(name) # pyOpenSSL>=0.13 153 | try: 154 | client_ssl.do_handshake() 155 | client_ssl.shutdown() 156 | except SSL.Error as error: 157 | raise errors.Error(error) 158 | return client_ssl.get_peer_certificate() 159 | 160 | def make_csr(private_key_pem, domains, must_staple=False): 161 | """Generate a CSR containing a list of domains as subjectAltNames. 162 | 163 | :param buffer private_key_pem: Private key, in PEM PKCS#8 format. 164 | :param list domains: List of DNS names to include in subjectAltNames of CSR. 165 | :param bool must_staple: Whether to include the TLS Feature extension (aka 166 | OCSP Must Staple: https://tools.ietf.org/html/rfc7633). 167 | :returns: buffer PEM-encoded Certificate Signing Request. 168 | """ 169 | private_key = crypto.load_privatekey( 170 | crypto.FILETYPE_PEM, private_key_pem) 171 | csr = crypto.X509Req() 172 | extensions = [ 173 | crypto.X509Extension( 174 | b'subjectAltName', 175 | critical=False, 176 | value=', '.join('DNS:' + d for d in domains).encode('ascii') 177 | ), 178 | ] 179 | if must_staple: 180 | extensions.append(crypto.X509Extension( 181 | b"1.3.6.1.5.5.7.1.24", 182 | critical=False, 183 | value=b"DER:30:03:02:01:05")) 184 | csr.add_extensions(extensions) 185 | csr.set_pubkey(private_key) 186 | csr.set_version(2) 187 | csr.sign(private_key, 'sha256') 188 | return crypto.dump_certificate_request( 189 | crypto.FILETYPE_PEM, csr) 190 | 191 | def _pyopenssl_cert_or_req_all_names(loaded_cert_or_req): 192 | common_name = loaded_cert_or_req.get_subject().CN 193 | sans = _pyopenssl_cert_or_req_san(loaded_cert_or_req) 194 | 195 | if common_name is None: 196 | return sans 197 | return [common_name] + [d for d in sans if d != common_name] 198 | 199 | def _pyopenssl_cert_or_req_san(cert_or_req): 200 | """Get Subject Alternative Names from certificate or CSR using pyOpenSSL. 201 | 202 | .. todo:: Implement directly in PyOpenSSL! 203 | 204 | .. note:: Although this is `acme` internal API, it is used by 205 | `letsencrypt`. 206 | 207 | :param cert_or_req: Certificate or CSR. 208 | :type cert_or_req: `OpenSSL.crypto.X509` or `OpenSSL.crypto.X509Req`. 209 | 210 | :returns: A list of Subject Alternative Names. 211 | :rtype: `list` of `unicode` 212 | 213 | """ 214 | # This function finds SANs by dumping the certificate/CSR to text and 215 | # searching for "X509v3 Subject Alternative Name" in the text. This method 216 | # is used to support PyOpenSSL version 0.13 where the 217 | # `_subjectAltNameString` and `get_extensions` methods are not available 218 | # for CSRs. 219 | 220 | # constants based on PyOpenSSL certificate/CSR text dump 221 | part_separator = ":" 222 | parts_separator = ", " 223 | prefix = "DNS" + part_separator 224 | 225 | if isinstance(cert_or_req, crypto.X509): 226 | # pylint: disable=line-too-long 227 | func = crypto.dump_certificate # type: Union[Callable[[int, crypto.X509Req], bytes], Callable[[int, crypto.X509], bytes]] 228 | else: 229 | func = crypto.dump_certificate_request 230 | text = func(crypto.FILETYPE_TEXT, cert_or_req).decode("utf-8") 231 | # WARNING: this function does not support multiple SANs extensions. 232 | # Multiple X509v3 extensions of the same type is disallowed by RFC 5280. 233 | match = re.search(r"X509v3 Subject Alternative Name:(?: critical)?\s*(.*)", text) 234 | # WARNING: this function assumes that no SAN can include 235 | # parts_separator, hence the split! 236 | sans_parts = [] if match is None else match.group(1).split(parts_separator) 237 | 238 | return [part.split(part_separator)[1] 239 | for part in sans_parts if part.startswith(prefix)] 240 | 241 | 242 | def gen_ss_cert(key, domains, not_before=None, 243 | validity=(7 * 24 * 60 * 60), force_san=True): 244 | """Generate new self-signed certificate. 245 | 246 | :type domains: `list` of `unicode` 247 | :param OpenSSL.crypto.PKey key: 248 | :param bool force_san: 249 | 250 | If more than one domain is provided, all of the domains are put into 251 | ``subjectAltName`` X.509 extension and first domain is set as the 252 | subject CN. If only one domain is provided no ``subjectAltName`` 253 | extension is used, unless `force_san` is ``True``. 254 | 255 | """ 256 | assert domains, "Must provide one or more hostnames for the cert." 257 | cert = crypto.X509() 258 | cert.set_serial_number(int(binascii.hexlify(os.urandom(16)), 16)) 259 | cert.set_version(2) 260 | 261 | extensions = [ 262 | crypto.X509Extension( 263 | b"basicConstraints", True, b"CA:TRUE, pathlen:0"), 264 | ] 265 | 266 | cert.get_subject().CN = domains[0] 267 | # TODO: what to put into cert.get_subject()? 268 | cert.set_issuer(cert.get_subject()) 269 | 270 | if force_san or len(domains) > 1: 271 | extensions.append(crypto.X509Extension( 272 | b"subjectAltName", 273 | critical=False, 274 | value=b", ".join(b"DNS:" + d.encode() for d in domains) 275 | )) 276 | 277 | cert.add_extensions(extensions) 278 | 279 | cert.gmtime_adj_notBefore(0 if not_before is None else not_before) 280 | cert.gmtime_adj_notAfter(validity) 281 | 282 | cert.set_pubkey(key) 283 | cert.sign(key, "sha256") 284 | return cert 285 | 286 | def dump_pyopenssl_chain(chain, filetype=crypto.FILETYPE_PEM): 287 | """Dump certificate chain into a bundle. 288 | 289 | :param list chain: List of `OpenSSL.crypto.X509` (or wrapped in 290 | :class:`josepy.util.ComparableX509`). 291 | 292 | :returns: certificate chain bundle 293 | :rtype: bytes 294 | 295 | """ 296 | # XXX: returns empty string when no chain is available, which 297 | # shuts up RenewableCert, but might not be the best solution... 298 | 299 | def _dump_cert(cert): 300 | if isinstance(cert, jose.ComparableX509): 301 | # pylint: disable=protected-access 302 | cert = cert.wrapped 303 | return crypto.dump_certificate(filetype, cert) 304 | 305 | # assumes that OpenSSL.crypto.dump_certificate includes ending 306 | # newline character 307 | return b"".join(_dump_cert(cert) for cert in chain) 308 | -------------------------------------------------------------------------------- /acme/challenges.py: -------------------------------------------------------------------------------- 1 | """ACME Identifier Validation Challenges.""" 2 | import abc 3 | import functools 4 | import hashlib 5 | import logging 6 | 7 | from cryptography.hazmat.primitives import hashes # type: ignore 8 | import josepy as jose 9 | import requests 10 | import six 11 | 12 | from acme import fields 13 | 14 | logger = logging.getLogger(__name__) 15 | 16 | 17 | class Challenge(jose.TypedJSONObjectWithFields): 18 | # _fields_to_partial_json 19 | """ACME challenge.""" 20 | TYPES = {} # type: dict 21 | 22 | @classmethod 23 | def from_json(cls, jobj): 24 | try: 25 | return super(Challenge, cls).from_json(jobj) 26 | except jose.UnrecognizedTypeError as error: 27 | logger.debug(error) 28 | return UnrecognizedChallenge.from_json(jobj) 29 | 30 | 31 | class ChallengeResponse(jose.TypedJSONObjectWithFields): 32 | # _fields_to_partial_json 33 | """ACME challenge response.""" 34 | TYPES = {} # type: dict 35 | resource_type = 'challenge' 36 | resource = fields.Resource(resource_type) 37 | 38 | 39 | class UnrecognizedChallenge(Challenge): 40 | """Unrecognized challenge. 41 | 42 | ACME specification defines a generic framework for challenges and 43 | defines some standard challenges that are implemented in this 44 | module. However, other implementations (including peers) might 45 | define additional challenge types, which should be ignored if 46 | unrecognized. 47 | 48 | :ivar jobj: Original JSON decoded object. 49 | 50 | """ 51 | 52 | def __init__(self, jobj): 53 | super(UnrecognizedChallenge, self).__init__() 54 | object.__setattr__(self, "jobj", jobj) 55 | 56 | def to_partial_json(self): 57 | return self.jobj # pylint: disable=no-member 58 | 59 | @classmethod 60 | def from_json(cls, jobj): 61 | return cls(jobj) 62 | 63 | 64 | class _TokenChallenge(Challenge): 65 | """Challenge with token. 66 | 67 | :ivar bytes token: 68 | 69 | """ 70 | TOKEN_SIZE = 128 / 8 # Based on the entropy value from the spec 71 | """Minimum size of the :attr:`token` in bytes.""" 72 | 73 | # TODO: acme-spec doesn't specify token as base64-encoded value 74 | token = jose.Field( 75 | "token", encoder=jose.encode_b64jose, decoder=functools.partial( 76 | jose.decode_b64jose, size=TOKEN_SIZE, minimum=True)) 77 | 78 | # XXX: rename to ~token_good_for_url 79 | @property 80 | def good_token(self): # XXX: @token.decoder 81 | """Is `token` good? 82 | 83 | .. todo:: acme-spec wants "It MUST NOT contain any non-ASCII 84 | characters", but it should also warrant that it doesn't 85 | contain ".." or "/"... 86 | 87 | """ 88 | # TODO: check that path combined with uri does not go above 89 | # URI_ROOT_PATH! 90 | # pylint: disable=unsupported-membership-test 91 | return b'..' not in self.token and b'/' not in self.token 92 | 93 | 94 | class KeyAuthorizationChallengeResponse(ChallengeResponse): 95 | """Response to Challenges based on Key Authorization. 96 | 97 | :param unicode key_authorization: 98 | 99 | """ 100 | key_authorization = jose.Field("keyAuthorization") 101 | thumbprint_hash_function = hashes.SHA256 102 | 103 | def verify(self, chall, account_public_key): 104 | """Verify the key authorization. 105 | 106 | :param KeyAuthorization chall: Challenge that corresponds to 107 | this response. 108 | :param JWK account_public_key: 109 | 110 | :return: ``True`` iff verification of the key authorization was 111 | successful. 112 | :rtype: bool 113 | 114 | """ 115 | parts = self.key_authorization.split('.') 116 | if len(parts) != 2: 117 | logger.debug("Key authorization (%r) is not well formed", 118 | self.key_authorization) 119 | return False 120 | 121 | if parts[0] != chall.encode("token"): 122 | logger.debug("Mismatching token in key authorization: " 123 | "%r instead of %r", parts[0], chall.encode("token")) 124 | return False 125 | 126 | thumbprint = jose.b64encode(account_public_key.thumbprint( 127 | hash_function=self.thumbprint_hash_function)).decode() 128 | if parts[1] != thumbprint: 129 | logger.debug("Mismatching thumbprint in key authorization: " 130 | "%r instead of %r", parts[0], thumbprint) 131 | return False 132 | 133 | return True 134 | 135 | def to_partial_json(self): 136 | jobj = super(KeyAuthorizationChallengeResponse, self).to_partial_json() 137 | jobj.pop('keyAuthorization', None) 138 | return jobj 139 | 140 | 141 | @six.add_metaclass(abc.ABCMeta) 142 | class KeyAuthorizationChallenge(_TokenChallenge): 143 | """Challenge based on Key Authorization. 144 | 145 | :param response_cls: Subclass of `KeyAuthorizationChallengeResponse` 146 | that will be used to generate `response`. 147 | :param str typ: type of the challenge 148 | """ 149 | typ = NotImplemented 150 | response_cls = NotImplemented 151 | thumbprint_hash_function = ( 152 | KeyAuthorizationChallengeResponse.thumbprint_hash_function) 153 | 154 | def key_authorization(self, account_key): 155 | """Generate Key Authorization. 156 | 157 | :param JWK account_key: 158 | :rtype unicode: 159 | 160 | """ 161 | return self.encode("token") + "." + jose.b64encode( 162 | account_key.thumbprint( 163 | hash_function=self.thumbprint_hash_function)).decode() 164 | 165 | def response(self, account_key): 166 | """Generate response to the challenge. 167 | 168 | :param JWK account_key: 169 | 170 | :returns: Response (initialized `response_cls`) to the challenge. 171 | :rtype: KeyAuthorizationChallengeResponse 172 | 173 | """ 174 | return self.response_cls( # pylint: disable=not-callable 175 | key_authorization=self.key_authorization(account_key)) 176 | 177 | @abc.abstractmethod 178 | def validation(self, account_key, **kwargs): 179 | """Generate validation for the challenge. 180 | 181 | Subclasses must implement this method, but they are likely to 182 | return completely different data structures, depending on what's 183 | necessary to complete the challenge. Interpretation of that 184 | return value must be known to the caller. 185 | 186 | :param JWK account_key: 187 | :returns: Challenge-specific validation. 188 | 189 | """ 190 | raise NotImplementedError() # pragma: no cover 191 | 192 | def response_and_validation(self, account_key, *args, **kwargs): 193 | """Generate response and validation. 194 | 195 | Convenience function that return results of `response` and 196 | `validation`. 197 | 198 | :param JWK account_key: 199 | :rtype: tuple 200 | 201 | """ 202 | return (self.response(account_key), 203 | self.validation(account_key, *args, **kwargs)) 204 | 205 | 206 | @ChallengeResponse.register 207 | class DNS01Response(KeyAuthorizationChallengeResponse): 208 | """ACME dns-01 challenge response.""" 209 | typ = "dns-01" 210 | 211 | def simple_verify(self, chall, domain, account_public_key): # pylint: disable=unused-argument 212 | """Simple verify. 213 | 214 | This method no longer checks DNS records and is a simple wrapper 215 | around `KeyAuthorizationChallengeResponse.verify`. 216 | 217 | :param challenges.DNS01 chall: Corresponding challenge. 218 | :param unicode domain: Domain name being verified. 219 | :param JWK account_public_key: Public key for the key pair 220 | being authorized. 221 | 222 | :return: ``True`` iff verification of the key authorization was 223 | successful. 224 | :rtype: bool 225 | 226 | """ 227 | verified = self.verify(chall, account_public_key) 228 | if not verified: 229 | logger.debug("Verification of key authorization in response failed") 230 | return verified 231 | 232 | 233 | @Challenge.register 234 | class DNS01(KeyAuthorizationChallenge): 235 | """ACME dns-01 challenge.""" 236 | response_cls = DNS01Response 237 | typ = response_cls.typ 238 | 239 | LABEL = "_acme-challenge" 240 | """Label clients prepend to the domain name being validated.""" 241 | 242 | def validation(self, account_key, **unused_kwargs): 243 | """Generate validation. 244 | 245 | :param JWK account_key: 246 | :rtype: unicode 247 | 248 | """ 249 | return jose.b64encode(hashlib.sha256(self.key_authorization( 250 | account_key).encode("utf-8")).digest()).decode() 251 | 252 | def validation_domain_name(self, name): 253 | """Domain name for TXT validation record. 254 | 255 | :param unicode name: Domain name being validated. 256 | 257 | """ 258 | return "{0}.{1}".format(self.LABEL, name) 259 | 260 | 261 | @ChallengeResponse.register 262 | class HTTP01Response(KeyAuthorizationChallengeResponse): 263 | """ACME http-01 challenge response.""" 264 | typ = "http-01" 265 | 266 | PORT = 80 267 | """Verification port as defined by the protocol. 268 | 269 | You can override it (e.g. for testing) by passing ``port`` to 270 | `simple_verify`. 271 | 272 | """ 273 | 274 | WHITESPACE_CUTSET = "\n\r\t " 275 | """Whitespace characters which should be ignored at the end of the body.""" 276 | 277 | def simple_verify(self, chall, domain, account_public_key, port=None): 278 | """Simple verify. 279 | 280 | :param challenges.SimpleHTTP chall: Corresponding challenge. 281 | :param unicode domain: Domain name being verified. 282 | :param JWK account_public_key: Public key for the key pair 283 | being authorized. 284 | :param int port: Port used in the validation. 285 | 286 | :returns: ``True`` iff validation with the files currently served by the 287 | HTTP server is successful. 288 | :rtype: bool 289 | 290 | """ 291 | if not self.verify(chall, account_public_key): 292 | logger.debug("Verification of key authorization in response failed") 293 | return False 294 | 295 | # TODO: ACME specification defines URI template that doesn't 296 | # allow to use a custom port... Make sure port is not in the 297 | # request URI, if it's standard. 298 | if port is not None and port != self.PORT: 299 | logger.warning( 300 | "Using non-standard port for http-01 verification: %s", port) 301 | domain += ":{0}".format(port) 302 | 303 | uri = chall.uri(domain) 304 | logger.debug("Verifying %s at %s...", chall.typ, uri) 305 | try: 306 | http_response = requests.get(uri) 307 | except requests.exceptions.RequestException as error: 308 | logger.error("Unable to reach %s: %s", uri, error) 309 | return False 310 | logger.debug("Received %s: %s. Headers: %s", http_response, 311 | http_response.text, http_response.headers) 312 | 313 | challenge_response = http_response.text.rstrip(self.WHITESPACE_CUTSET) 314 | if self.key_authorization != challenge_response: 315 | logger.debug("Key authorization from response (%r) doesn't match " 316 | "HTTP response (%r)", self.key_authorization, 317 | challenge_response) 318 | return False 319 | 320 | return True 321 | 322 | 323 | @Challenge.register 324 | class HTTP01(KeyAuthorizationChallenge): 325 | """ACME http-01 challenge.""" 326 | response_cls = HTTP01Response 327 | typ = response_cls.typ 328 | 329 | URI_ROOT_PATH = ".well-known/acme-challenge" 330 | """URI root path for the server provisioned resource.""" 331 | 332 | @property 333 | def path(self): 334 | """Path (starting with '/') for provisioned resource. 335 | 336 | :rtype: string 337 | 338 | """ 339 | return '/' + self.URI_ROOT_PATH + '/' + self.encode('token') 340 | 341 | def uri(self, domain): 342 | """Create an URI to the provisioned resource. 343 | 344 | Forms an URI to the HTTPS server provisioned resource 345 | (containing :attr:`~SimpleHTTP.token`). 346 | 347 | :param unicode domain: Domain name being verified. 348 | :rtype: string 349 | 350 | """ 351 | return "http://" + domain + self.path 352 | 353 | def validation(self, account_key, **unused_kwargs): 354 | """Generate validation. 355 | 356 | :param JWK account_key: 357 | :rtype: unicode 358 | 359 | """ 360 | return self.key_authorization(account_key) 361 | 362 | 363 | @ChallengeResponse.register 364 | class TLSALPN01Response(KeyAuthorizationChallengeResponse): 365 | """ACME TLS-ALPN-01 challenge response. 366 | 367 | This class only allows initiating a TLS-ALPN-01 challenge returned from the 368 | CA. Full support for responding to TLS-ALPN-01 challenges by generating and 369 | serving the expected response certificate is not currently provided. 370 | """ 371 | typ = "tls-alpn-01" 372 | 373 | 374 | @Challenge.register 375 | class TLSALPN01(KeyAuthorizationChallenge): 376 | """ACME tls-alpn-01 challenge. 377 | 378 | This class simply allows parsing the TLS-ALPN-01 challenge returned from 379 | the CA. Full TLS-ALPN-01 support is not currently provided. 380 | 381 | """ 382 | typ = "tls-alpn-01" 383 | response_cls = TLSALPN01Response 384 | 385 | def validation(self, account_key, **kwargs): 386 | """Generate validation for the challenge.""" 387 | raise NotImplementedError() 388 | 389 | 390 | @Challenge.register 391 | class DNS(_TokenChallenge): 392 | """ACME "dns" challenge.""" 393 | typ = "dns" 394 | 395 | LABEL = "_acme-challenge" 396 | """Label clients prepend to the domain name being validated.""" 397 | 398 | def gen_validation(self, account_key, alg=jose.RS256, **kwargs): 399 | """Generate validation. 400 | 401 | :param .JWK account_key: Private account key. 402 | :param .JWA alg: 403 | 404 | :returns: This challenge wrapped in `.JWS` 405 | :rtype: .JWS 406 | 407 | """ 408 | return jose.JWS.sign( 409 | payload=self.json_dumps(sort_keys=True).encode('utf-8'), 410 | key=account_key, alg=alg, **kwargs) 411 | 412 | def check_validation(self, validation, account_public_key): 413 | """Check validation. 414 | 415 | :param JWS validation: 416 | :param JWK account_public_key: 417 | :rtype: bool 418 | 419 | """ 420 | if not validation.verify(key=account_public_key): 421 | return False 422 | try: 423 | return self == self.json_loads( 424 | validation.payload.decode('utf-8')) 425 | except jose.DeserializationError as error: 426 | logger.debug("Checking validation for DNS failed: %s", error) 427 | return False 428 | 429 | def gen_response(self, account_key, **kwargs): 430 | """Generate response. 431 | 432 | :param .JWK account_key: Private account key. 433 | :param .JWA alg: 434 | 435 | :rtype: DNSResponse 436 | 437 | """ 438 | return DNSResponse(validation=self.gen_validation( 439 | account_key, **kwargs)) 440 | 441 | def validation_domain_name(self, name): 442 | """Domain name for TXT validation record. 443 | 444 | :param unicode name: Domain name being validated. 445 | 446 | """ 447 | return "{0}.{1}".format(self.LABEL, name) 448 | 449 | 450 | @ChallengeResponse.register 451 | class DNSResponse(ChallengeResponse): 452 | """ACME "dns" challenge response. 453 | 454 | :param JWS validation: 455 | 456 | """ 457 | typ = "dns" 458 | 459 | validation = jose.Field("validation", decoder=jose.JWS.from_json) 460 | 461 | def check_validation(self, chall, account_public_key): 462 | """Check validation. 463 | 464 | :param challenges.DNS chall: 465 | :param JWK account_public_key: 466 | 467 | :rtype: bool 468 | 469 | """ 470 | return chall.check_validation(self.validation, account_public_key) 471 | -------------------------------------------------------------------------------- /tests/challenges_test.py: -------------------------------------------------------------------------------- 1 | """Tests for acme.challenges.""" 2 | import unittest 3 | 4 | import josepy as jose 5 | import mock 6 | import requests 7 | from six.moves.urllib import parse as urllib_parse 8 | 9 | import test_util 10 | 11 | CERT = test_util.load_comparable_cert('cert.pem') 12 | KEY = jose.JWKRSA(key=test_util.load_rsa_private_key('rsa512_key.pem')) 13 | 14 | 15 | class ChallengeTest(unittest.TestCase): 16 | 17 | def test_from_json_unrecognized(self): 18 | from acme.challenges import Challenge 19 | from acme.challenges import UnrecognizedChallenge 20 | chall = UnrecognizedChallenge({"type": "foo"}) 21 | self.assertEqual(chall, Challenge.from_json(chall.jobj)) 22 | 23 | 24 | class UnrecognizedChallengeTest(unittest.TestCase): 25 | 26 | def setUp(self): 27 | from acme.challenges import UnrecognizedChallenge 28 | self.jobj = {"type": "foo"} 29 | self.chall = UnrecognizedChallenge(self.jobj) 30 | 31 | def test_to_partial_json(self): 32 | self.assertEqual(self.jobj, self.chall.to_partial_json()) 33 | 34 | def test_from_json(self): 35 | from acme.challenges import UnrecognizedChallenge 36 | self.assertEqual( 37 | self.chall, UnrecognizedChallenge.from_json(self.jobj)) 38 | 39 | 40 | class KeyAuthorizationChallengeResponseTest(unittest.TestCase): 41 | 42 | def setUp(self): 43 | def _encode(name): 44 | assert name == "token" 45 | return "foo" 46 | self.chall = mock.Mock() 47 | self.chall.encode.side_effect = _encode 48 | 49 | def test_verify_ok(self): 50 | from acme.challenges import KeyAuthorizationChallengeResponse 51 | response = KeyAuthorizationChallengeResponse( 52 | key_authorization='foo.oKGqedy-b-acd5eoybm2f-NVFxvyOoET5CNy3xnv8WY') 53 | self.assertTrue(response.verify(self.chall, KEY.public_key())) 54 | 55 | def test_verify_wrong_token(self): 56 | from acme.challenges import KeyAuthorizationChallengeResponse 57 | response = KeyAuthorizationChallengeResponse( 58 | key_authorization='bar.oKGqedy-b-acd5eoybm2f-NVFxvyOoET5CNy3xnv8WY') 59 | self.assertFalse(response.verify(self.chall, KEY.public_key())) 60 | 61 | def test_verify_wrong_thumbprint(self): 62 | from acme.challenges import KeyAuthorizationChallengeResponse 63 | response = KeyAuthorizationChallengeResponse( 64 | key_authorization='foo.oKGqedy-b-acd5eoybm2f-NVFxv') 65 | self.assertFalse(response.verify(self.chall, KEY.public_key())) 66 | 67 | def test_verify_wrong_form(self): 68 | from acme.challenges import KeyAuthorizationChallengeResponse 69 | response = KeyAuthorizationChallengeResponse( 70 | key_authorization='.foo.oKGqedy-b-acd5eoybm2f-' 71 | 'NVFxvyOoET5CNy3xnv8WY') 72 | self.assertFalse(response.verify(self.chall, KEY.public_key())) 73 | 74 | 75 | class DNS01ResponseTest(unittest.TestCase): 76 | 77 | def setUp(self): 78 | from acme.challenges import DNS01Response 79 | self.msg = DNS01Response(key_authorization=u'foo') 80 | self.jmsg = { 81 | 'resource': 'challenge', 82 | 'type': 'dns-01', 83 | 'keyAuthorization': u'foo', 84 | } 85 | 86 | from acme.challenges import DNS01 87 | self.chall = DNS01(token=(b'x' * 16)) 88 | self.response = self.chall.response(KEY) 89 | 90 | def test_to_partial_json(self): 91 | self.assertEqual({k: v for k, v in self.jmsg.items() if k != 'keyAuthorization'}, 92 | self.msg.to_partial_json()) 93 | 94 | def test_from_json(self): 95 | from acme.challenges import DNS01Response 96 | self.assertEqual(self.msg, DNS01Response.from_json(self.jmsg)) 97 | 98 | def test_from_json_hashable(self): 99 | from acme.challenges import DNS01Response 100 | hash(DNS01Response.from_json(self.jmsg)) 101 | 102 | def test_simple_verify_failure(self): 103 | key2 = jose.JWKRSA.load(test_util.load_vector('rsa256_key.pem')) 104 | public_key = key2.public_key() 105 | verified = self.response.simple_verify(self.chall, "local", public_key) 106 | self.assertFalse(verified) 107 | 108 | def test_simple_verify_success(self): 109 | public_key = KEY.public_key() 110 | verified = self.response.simple_verify(self.chall, "local", public_key) 111 | self.assertTrue(verified) 112 | 113 | 114 | class DNS01Test(unittest.TestCase): 115 | 116 | def setUp(self): 117 | from acme.challenges import DNS01 118 | self.msg = DNS01(token=jose.decode_b64jose( 119 | 'evaGxfADs6pSRb2LAv9IZf17Dt3juxGJ+PCt92wr+oA')) 120 | self.jmsg = { 121 | 'type': 'dns-01', 122 | 'token': 'evaGxfADs6pSRb2LAv9IZf17Dt3juxGJ-PCt92wr-oA', 123 | } 124 | 125 | def test_validation_domain_name(self): 126 | self.assertEqual('_acme-challenge.www.example.com', 127 | self.msg.validation_domain_name('www.example.com')) 128 | 129 | def test_validation(self): 130 | self.assertEqual( 131 | "rAa7iIg4K2y63fvUhCfy8dP1Xl7wEhmQq0oChTcE3Zk", 132 | self.msg.validation(KEY)) 133 | 134 | def test_to_partial_json(self): 135 | self.assertEqual(self.jmsg, self.msg.to_partial_json()) 136 | 137 | def test_from_json(self): 138 | from acme.challenges import DNS01 139 | self.assertEqual(self.msg, DNS01.from_json(self.jmsg)) 140 | 141 | def test_from_json_hashable(self): 142 | from acme.challenges import DNS01 143 | hash(DNS01.from_json(self.jmsg)) 144 | 145 | 146 | class HTTP01ResponseTest(unittest.TestCase): 147 | 148 | def setUp(self): 149 | from acme.challenges import HTTP01Response 150 | self.msg = HTTP01Response(key_authorization=u'foo') 151 | self.jmsg = { 152 | 'resource': 'challenge', 153 | 'type': 'http-01', 154 | 'keyAuthorization': u'foo', 155 | } 156 | 157 | from acme.challenges import HTTP01 158 | self.chall = HTTP01(token=(b'x' * 16)) 159 | self.response = self.chall.response(KEY) 160 | 161 | def test_to_partial_json(self): 162 | self.assertEqual({k: v for k, v in self.jmsg.items() if k != 'keyAuthorization'}, 163 | self.msg.to_partial_json()) 164 | 165 | def test_from_json(self): 166 | from acme.challenges import HTTP01Response 167 | self.assertEqual( 168 | self.msg, HTTP01Response.from_json(self.jmsg)) 169 | 170 | def test_from_json_hashable(self): 171 | from acme.challenges import HTTP01Response 172 | hash(HTTP01Response.from_json(self.jmsg)) 173 | 174 | def test_simple_verify_bad_key_authorization(self): 175 | key2 = jose.JWKRSA.load(test_util.load_vector('rsa256_key.pem')) 176 | self.response.simple_verify(self.chall, "local", key2.public_key()) 177 | 178 | @mock.patch("acme.challenges.requests.get") 179 | def test_simple_verify_good_validation(self, mock_get): 180 | validation = self.chall.validation(KEY) 181 | mock_get.return_value = mock.MagicMock(text=validation) 182 | self.assertTrue(self.response.simple_verify( 183 | self.chall, "local", KEY.public_key())) 184 | mock_get.assert_called_once_with(self.chall.uri("local")) 185 | 186 | @mock.patch("acme.challenges.requests.get") 187 | def test_simple_verify_bad_validation(self, mock_get): 188 | mock_get.return_value = mock.MagicMock(text="!") 189 | self.assertFalse(self.response.simple_verify( 190 | self.chall, "local", KEY.public_key())) 191 | 192 | @mock.patch("acme.challenges.requests.get") 193 | def test_simple_verify_whitespace_validation(self, mock_get): 194 | from acme.challenges import HTTP01Response 195 | mock_get.return_value = mock.MagicMock( 196 | text=(self.chall.validation(KEY) + 197 | HTTP01Response.WHITESPACE_CUTSET)) 198 | self.assertTrue(self.response.simple_verify( 199 | self.chall, "local", KEY.public_key())) 200 | mock_get.assert_called_once_with(self.chall.uri("local")) 201 | 202 | @mock.patch("acme.challenges.requests.get") 203 | def test_simple_verify_connection_error(self, mock_get): 204 | mock_get.side_effect = requests.exceptions.RequestException 205 | self.assertFalse(self.response.simple_verify( 206 | self.chall, "local", KEY.public_key())) 207 | 208 | @mock.patch("acme.challenges.requests.get") 209 | def test_simple_verify_port(self, mock_get): 210 | self.response.simple_verify( 211 | self.chall, domain="local", 212 | account_public_key=KEY.public_key(), port=8080) 213 | self.assertEqual("local:8080", urllib_parse.urlparse( 214 | mock_get.mock_calls[0][1][0]).netloc) 215 | 216 | 217 | class HTTP01Test(unittest.TestCase): 218 | 219 | def setUp(self): 220 | from acme.challenges import HTTP01 221 | self.msg = HTTP01( 222 | token=jose.decode_b64jose( 223 | 'evaGxfADs6pSRb2LAv9IZf17Dt3juxGJ+PCt92wr+oA')) 224 | self.jmsg = { 225 | 'type': 'http-01', 226 | 'token': 'evaGxfADs6pSRb2LAv9IZf17Dt3juxGJ-PCt92wr-oA', 227 | } 228 | 229 | def test_path(self): 230 | self.assertEqual(self.msg.path, '/.well-known/acme-challenge/' 231 | 'evaGxfADs6pSRb2LAv9IZf17Dt3juxGJ-PCt92wr-oA') 232 | 233 | def test_uri(self): 234 | self.assertEqual( 235 | 'http://example.com/.well-known/acme-challenge/' 236 | 'evaGxfADs6pSRb2LAv9IZf17Dt3juxGJ-PCt92wr-oA', 237 | self.msg.uri('example.com')) 238 | 239 | def test_to_partial_json(self): 240 | self.assertEqual(self.jmsg, self.msg.to_partial_json()) 241 | 242 | def test_from_json(self): 243 | from acme.challenges import HTTP01 244 | self.assertEqual(self.msg, HTTP01.from_json(self.jmsg)) 245 | 246 | def test_from_json_hashable(self): 247 | from acme.challenges import HTTP01 248 | hash(HTTP01.from_json(self.jmsg)) 249 | 250 | def test_good_token(self): 251 | self.assertTrue(self.msg.good_token) 252 | self.assertFalse( 253 | self.msg.update(token=b'..').good_token) 254 | 255 | 256 | class TLSALPN01ResponseTest(unittest.TestCase): 257 | 258 | def setUp(self): 259 | from acme.challenges import TLSALPN01Response 260 | self.msg = TLSALPN01Response(key_authorization=u'foo') 261 | self.jmsg = { 262 | 'resource': 'challenge', 263 | 'type': 'tls-alpn-01', 264 | 'keyAuthorization': u'foo', 265 | } 266 | 267 | from acme.challenges import TLSALPN01 268 | self.chall = TLSALPN01(token=(b'x' * 16)) 269 | self.response = self.chall.response(KEY) 270 | 271 | def test_to_partial_json(self): 272 | self.assertEqual({k: v for k, v in self.jmsg.items() if k != 'keyAuthorization'}, 273 | self.msg.to_partial_json()) 274 | 275 | def test_from_json(self): 276 | from acme.challenges import TLSALPN01Response 277 | self.assertEqual(self.msg, TLSALPN01Response.from_json(self.jmsg)) 278 | 279 | def test_from_json_hashable(self): 280 | from acme.challenges import TLSALPN01Response 281 | hash(TLSALPN01Response.from_json(self.jmsg)) 282 | 283 | 284 | class TLSALPN01Test(unittest.TestCase): 285 | 286 | def setUp(self): 287 | from acme.challenges import TLSALPN01 288 | self.msg = TLSALPN01( 289 | token=jose.b64decode('a82d5ff8ef740d12881f6d3c2277ab2e')) 290 | self.jmsg = { 291 | 'type': 'tls-alpn-01', 292 | 'token': 'a82d5ff8ef740d12881f6d3c2277ab2e', 293 | } 294 | 295 | def test_to_partial_json(self): 296 | self.assertEqual(self.jmsg, self.msg.to_partial_json()) 297 | 298 | def test_from_json(self): 299 | from acme.challenges import TLSALPN01 300 | self.assertEqual(self.msg, TLSALPN01.from_json(self.jmsg)) 301 | 302 | def test_from_json_hashable(self): 303 | from acme.challenges import TLSALPN01 304 | hash(TLSALPN01.from_json(self.jmsg)) 305 | 306 | def test_from_json_invalid_token_length(self): 307 | from acme.challenges import TLSALPN01 308 | self.jmsg['token'] = jose.encode_b64jose(b'abcd') 309 | self.assertRaises( 310 | jose.DeserializationError, TLSALPN01.from_json, self.jmsg) 311 | 312 | def test_validation(self): 313 | self.assertRaises(NotImplementedError, self.msg.validation, KEY) 314 | 315 | 316 | class DNSTest(unittest.TestCase): 317 | 318 | def setUp(self): 319 | from acme.challenges import DNS 320 | self.msg = DNS(token=jose.b64decode( 321 | b'evaGxfADs6pSRb2LAv9IZf17Dt3juxGJ-PCt92wr-oA')) 322 | self.jmsg = { 323 | 'type': 'dns', 324 | 'token': 'evaGxfADs6pSRb2LAv9IZf17Dt3juxGJ-PCt92wr-oA', 325 | } 326 | 327 | def test_to_partial_json(self): 328 | self.assertEqual(self.jmsg, self.msg.to_partial_json()) 329 | 330 | def test_from_json(self): 331 | from acme.challenges import DNS 332 | self.assertEqual(self.msg, DNS.from_json(self.jmsg)) 333 | 334 | def test_from_json_hashable(self): 335 | from acme.challenges import DNS 336 | hash(DNS.from_json(self.jmsg)) 337 | 338 | def test_gen_check_validation(self): 339 | self.assertTrue(self.msg.check_validation( 340 | self.msg.gen_validation(KEY), KEY.public_key())) 341 | 342 | def test_gen_check_validation_wrong_key(self): 343 | key2 = jose.JWKRSA.load(test_util.load_vector('rsa1024_key.pem')) 344 | self.assertFalse(self.msg.check_validation( 345 | self.msg.gen_validation(KEY), key2.public_key())) 346 | 347 | def test_check_validation_wrong_payload(self): 348 | validations = tuple( 349 | jose.JWS.sign(payload=payload, alg=jose.RS256, key=KEY) 350 | for payload in (b'', b'{}') 351 | ) 352 | for validation in validations: 353 | self.assertFalse(self.msg.check_validation( 354 | validation, KEY.public_key())) 355 | 356 | def test_check_validation_wrong_fields(self): 357 | bad_validation = jose.JWS.sign( 358 | payload=self.msg.update( 359 | token=b'x' * 20).json_dumps().encode('utf-8'), 360 | alg=jose.RS256, key=KEY) 361 | self.assertFalse(self.msg.check_validation( 362 | bad_validation, KEY.public_key())) 363 | 364 | def test_gen_response(self): 365 | with mock.patch('acme.challenges.DNS.gen_validation') as mock_gen: 366 | mock_gen.return_value = mock.sentinel.validation 367 | response = self.msg.gen_response(KEY) 368 | from acme.challenges import DNSResponse 369 | self.assertTrue(isinstance(response, DNSResponse)) 370 | self.assertEqual(response.validation, mock.sentinel.validation) 371 | 372 | def test_validation_domain_name(self): 373 | self.assertEqual( 374 | '_acme-challenge.le.wtf', self.msg.validation_domain_name('le.wtf')) 375 | 376 | 377 | class DNSResponseTest(unittest.TestCase): 378 | 379 | def setUp(self): 380 | from acme.challenges import DNS 381 | self.chall = DNS(token=jose.b64decode( 382 | b"evaGxfADs6pSRb2LAv9IZf17Dt3juxGJ-PCt92wr-oA")) 383 | self.validation = jose.JWS.sign( 384 | payload=self.chall.json_dumps(sort_keys=True).encode(), 385 | key=KEY, alg=jose.RS256) 386 | 387 | from acme.challenges import DNSResponse 388 | self.msg = DNSResponse(validation=self.validation) 389 | self.jmsg_to = { 390 | 'resource': 'challenge', 391 | 'type': 'dns', 392 | 'validation': self.validation, 393 | } 394 | self.jmsg_from = { 395 | 'resource': 'challenge', 396 | 'type': 'dns', 397 | 'validation': self.validation.to_json(), 398 | } 399 | 400 | def test_to_partial_json(self): 401 | self.assertEqual(self.jmsg_to, self.msg.to_partial_json()) 402 | 403 | def test_from_json(self): 404 | from acme.challenges import DNSResponse 405 | self.assertEqual(self.msg, DNSResponse.from_json(self.jmsg_from)) 406 | 407 | def test_from_json_hashable(self): 408 | from acme.challenges import DNSResponse 409 | hash(DNSResponse.from_json(self.jmsg_from)) 410 | 411 | def test_check_validation(self): 412 | self.assertTrue( 413 | self.msg.check_validation(self.chall, KEY.public_key())) 414 | 415 | 416 | if __name__ == '__main__': 417 | unittest.main() # pragma: no cover 418 | -------------------------------------------------------------------------------- /tests/messages_test.py: -------------------------------------------------------------------------------- 1 | """Tests for acme.messages.""" 2 | import unittest 3 | 4 | import josepy as jose 5 | import mock 6 | 7 | from acme import challenges 8 | from acme.magic_typing import Dict # pylint: disable=unused-import, no-name-in-module 9 | import test_util 10 | 11 | CERT = test_util.load_comparable_cert('cert.der') 12 | CSR = test_util.load_comparable_csr('csr.der') 13 | KEY = test_util.load_rsa_private_key('rsa512_key.pem') 14 | 15 | 16 | class ErrorTest(unittest.TestCase): 17 | """Tests for acme.messages.Error.""" 18 | 19 | def setUp(self): 20 | from acme.messages import Error, ERROR_PREFIX 21 | self.error = Error.with_code('malformed', detail='foo', title='title') 22 | self.jobj = { 23 | 'detail': 'foo', 24 | 'title': 'some title', 25 | 'type': ERROR_PREFIX + 'malformed', 26 | } 27 | self.error_custom = Error(typ='custom', detail='bar') 28 | self.empty_error = Error() 29 | 30 | def test_default_typ(self): 31 | from acme.messages import Error 32 | self.assertEqual(Error().typ, 'about:blank') 33 | 34 | def test_from_json_empty(self): 35 | from acme.messages import Error 36 | self.assertEqual(Error(), Error.from_json('{}')) 37 | 38 | def test_from_json_hashable(self): 39 | from acme.messages import Error 40 | hash(Error.from_json(self.error.to_json())) 41 | 42 | def test_description(self): 43 | self.assertEqual('The request message was malformed', self.error.description) 44 | self.assertTrue(self.error_custom.description is None) 45 | 46 | def test_code(self): 47 | from acme.messages import Error 48 | self.assertEqual('malformed', self.error.code) 49 | self.assertEqual(None, self.error_custom.code) 50 | self.assertEqual(None, Error().code) 51 | 52 | def test_is_acme_error(self): 53 | from acme.messages import is_acme_error, Error 54 | self.assertTrue(is_acme_error(self.error)) 55 | self.assertFalse(is_acme_error(self.error_custom)) 56 | self.assertFalse(is_acme_error(Error())) 57 | self.assertFalse(is_acme_error(self.empty_error)) 58 | self.assertFalse(is_acme_error("must pet all the {dogs|rabbits}")) 59 | 60 | def test_unicode_error(self): 61 | from acme.messages import Error, is_acme_error 62 | arabic_error = Error.with_code( 63 | 'malformed', detail=u'\u0639\u062f\u0627\u0644\u0629', title='title') 64 | self.assertTrue(is_acme_error(arabic_error)) 65 | 66 | def test_with_code(self): 67 | from acme.messages import Error, is_acme_error 68 | self.assertTrue(is_acme_error(Error.with_code('badCSR'))) 69 | self.assertRaises(ValueError, Error.with_code, 'not an ACME error code') 70 | 71 | def test_str(self): 72 | self.assertEqual( 73 | str(self.error), 74 | u"{0.typ} :: {0.description} :: {0.detail} :: {0.title}" 75 | .format(self.error)) 76 | 77 | 78 | class ConstantTest(unittest.TestCase): 79 | """Tests for acme.messages._Constant.""" 80 | 81 | def setUp(self): 82 | from acme.messages import _Constant 83 | 84 | class MockConstant(_Constant): # pylint: disable=missing-docstring 85 | POSSIBLE_NAMES = {} # type: Dict 86 | 87 | self.MockConstant = MockConstant # pylint: disable=invalid-name 88 | self.const_a = MockConstant('a') 89 | self.const_b = MockConstant('b') 90 | 91 | def test_to_partial_json(self): 92 | self.assertEqual('a', self.const_a.to_partial_json()) 93 | self.assertEqual('b', self.const_b.to_partial_json()) 94 | 95 | def test_from_json(self): 96 | self.assertEqual(self.const_a, self.MockConstant.from_json('a')) 97 | self.assertRaises( 98 | jose.DeserializationError, self.MockConstant.from_json, 'c') 99 | 100 | def test_from_json_hashable(self): 101 | hash(self.MockConstant.from_json('a')) 102 | 103 | def test_repr(self): 104 | self.assertEqual('MockConstant(a)', repr(self.const_a)) 105 | self.assertEqual('MockConstant(b)', repr(self.const_b)) 106 | 107 | def test_equality(self): 108 | const_a_prime = self.MockConstant('a') 109 | self.assertFalse(self.const_a == self.const_b) 110 | self.assertTrue(self.const_a == const_a_prime) 111 | 112 | self.assertTrue(self.const_a != self.const_b) 113 | self.assertFalse(self.const_a != const_a_prime) 114 | 115 | 116 | class DirectoryTest(unittest.TestCase): 117 | """Tests for acme.messages.Directory.""" 118 | 119 | def setUp(self): 120 | from acme.messages import Directory 121 | self.dir = Directory({ 122 | 'new-reg': 'reg', 123 | mock.MagicMock(resource_type='new-cert'): 'cert', 124 | 'meta': Directory.Meta( 125 | terms_of_service='https://example.com/acme/terms', 126 | website='https://www.example.com/', 127 | caa_identities=['example.com'], 128 | ), 129 | }) 130 | 131 | def test_init_wrong_key_value_success(self): # pylint: disable=no-self-use 132 | from acme.messages import Directory 133 | Directory({'foo': 'bar'}) 134 | 135 | def test_getitem(self): 136 | self.assertEqual('reg', self.dir['new-reg']) 137 | from acme.messages import NewRegistration 138 | self.assertEqual('reg', self.dir[NewRegistration]) 139 | self.assertEqual('reg', self.dir[NewRegistration()]) 140 | 141 | def test_getitem_fails_with_key_error(self): 142 | self.assertRaises(KeyError, self.dir.__getitem__, 'foo') 143 | 144 | def test_getattr(self): 145 | self.assertEqual('reg', self.dir.new_reg) 146 | 147 | def test_getattr_fails_with_attribute_error(self): 148 | self.assertRaises(AttributeError, self.dir.__getattr__, 'foo') 149 | 150 | def test_to_json(self): 151 | self.assertEqual(self.dir.to_json(), { 152 | 'new-reg': 'reg', 153 | 'new-cert': 'cert', 154 | 'meta': { 155 | 'terms-of-service': 'https://example.com/acme/terms', 156 | 'website': 'https://www.example.com/', 157 | 'caaIdentities': ['example.com'], 158 | }, 159 | }) 160 | 161 | def test_from_json_deserialization_unknown_key_success(self): # pylint: disable=no-self-use 162 | from acme.messages import Directory 163 | Directory.from_json({'foo': 'bar'}) 164 | 165 | def test_iter_meta(self): 166 | result = False 167 | for k in self.dir.meta: 168 | if k == 'terms_of_service': 169 | result = self.dir.meta[k] == 'https://example.com/acme/terms' 170 | self.assertTrue(result) 171 | 172 | 173 | class ExternalAccountBindingTest(unittest.TestCase): 174 | def setUp(self): 175 | from acme.messages import Directory 176 | self.key = jose.jwk.JWKRSA(key=KEY.public_key()) 177 | self.kid = "kid-for-testing" 178 | self.hmac_key = "hmac-key-for-testing" 179 | self.dir = Directory({ 180 | 'newAccount': 'http://url/acme/new-account', 181 | }) 182 | 183 | def test_from_data(self): 184 | from acme.messages import ExternalAccountBinding 185 | eab = ExternalAccountBinding.from_data(self.key, self.kid, self.hmac_key, self.dir) 186 | 187 | self.assertEqual(len(eab), 3) 188 | self.assertEqual(sorted(eab.keys()), sorted(['protected', 'payload', 'signature'])) 189 | 190 | 191 | class RegistrationTest(unittest.TestCase): 192 | """Tests for acme.messages.Registration.""" 193 | 194 | def setUp(self): 195 | key = jose.jwk.JWKRSA(key=KEY.public_key()) 196 | contact = ( 197 | 'mailto:admin@foo.com', 198 | 'tel:1234', 199 | ) 200 | agreement = 'https://letsencrypt.org/terms' 201 | 202 | from acme.messages import Registration 203 | self.reg = Registration(key=key, contact=contact, agreement=agreement) 204 | self.reg_none = Registration() 205 | 206 | self.jobj_to = { 207 | 'contact': contact, 208 | 'agreement': agreement, 209 | 'key': key, 210 | } 211 | self.jobj_from = self.jobj_to.copy() 212 | self.jobj_from['key'] = key.to_json() 213 | 214 | def test_from_data(self): 215 | from acme.messages import Registration 216 | reg = Registration.from_data(phone='1234', email='admin@foo.com') 217 | self.assertEqual(reg.contact, ( 218 | 'tel:1234', 219 | 'mailto:admin@foo.com', 220 | )) 221 | 222 | def test_new_registration_from_data_with_eab(self): 223 | from acme.messages import NewRegistration, ExternalAccountBinding, Directory 224 | key = jose.jwk.JWKRSA(key=KEY.public_key()) 225 | kid = "kid-for-testing" 226 | hmac_key = "hmac-key-for-testing" 227 | directory = Directory({ 228 | 'newAccount': 'http://url/acme/new-account', 229 | }) 230 | eab = ExternalAccountBinding.from_data(key, kid, hmac_key, directory) 231 | reg = NewRegistration.from_data(email='admin@foo.com', external_account_binding=eab) 232 | self.assertEqual(reg.contact, ( 233 | 'mailto:admin@foo.com', 234 | )) 235 | self.assertEqual(sorted(reg.external_account_binding.keys()), 236 | sorted(['protected', 'payload', 'signature'])) 237 | 238 | def test_phones(self): 239 | self.assertEqual(('1234',), self.reg.phones) 240 | 241 | def test_emails(self): 242 | self.assertEqual(('admin@foo.com',), self.reg.emails) 243 | 244 | def test_to_partial_json(self): 245 | self.assertEqual(self.jobj_to, self.reg.to_partial_json()) 246 | 247 | def test_from_json(self): 248 | from acme.messages import Registration 249 | self.assertEqual(self.reg, Registration.from_json(self.jobj_from)) 250 | 251 | def test_from_json_hashable(self): 252 | from acme.messages import Registration 253 | hash(Registration.from_json(self.jobj_from)) 254 | 255 | 256 | class UpdateRegistrationTest(unittest.TestCase): 257 | """Tests for acme.messages.UpdateRegistration.""" 258 | 259 | def test_empty(self): 260 | from acme.messages import UpdateRegistration 261 | jstring = '{"resource": "reg"}' 262 | self.assertEqual(jstring, UpdateRegistration().json_dumps()) 263 | self.assertEqual( 264 | UpdateRegistration(), UpdateRegistration.json_loads(jstring)) 265 | 266 | 267 | class RegistrationResourceTest(unittest.TestCase): 268 | """Tests for acme.messages.RegistrationResource.""" 269 | 270 | def setUp(self): 271 | from acme.messages import RegistrationResource 272 | self.regr = RegistrationResource( 273 | body=mock.sentinel.body, uri=mock.sentinel.uri, 274 | terms_of_service=mock.sentinel.terms_of_service) 275 | 276 | def test_to_partial_json(self): 277 | self.assertEqual(self.regr.to_json(), { 278 | 'body': mock.sentinel.body, 279 | 'uri': mock.sentinel.uri, 280 | 'terms_of_service': mock.sentinel.terms_of_service, 281 | }) 282 | 283 | 284 | class ChallengeResourceTest(unittest.TestCase): 285 | """Tests for acme.messages.ChallengeResource.""" 286 | 287 | def test_uri(self): 288 | from acme.messages import ChallengeResource 289 | self.assertEqual('http://challb', ChallengeResource(body=mock.MagicMock( 290 | uri='http://challb'), authzr_uri='http://authz').uri) 291 | 292 | 293 | class ChallengeBodyTest(unittest.TestCase): 294 | """Tests for acme.messages.ChallengeBody.""" 295 | 296 | def setUp(self): 297 | self.chall = challenges.DNS(token=jose.b64decode( 298 | 'evaGxfADs6pSRb2LAv9IZf17Dt3juxGJ-PCt92wr-oA')) 299 | 300 | from acme.messages import ChallengeBody 301 | from acme.messages import Error 302 | from acme.messages import STATUS_INVALID 303 | self.status = STATUS_INVALID 304 | error = Error.with_code('serverInternal', detail='Unable to communicate with DNS server') 305 | self.challb = ChallengeBody( 306 | uri='http://challb', chall=self.chall, status=self.status, 307 | error=error) 308 | 309 | self.jobj_to = { 310 | 'uri': 'http://challb', 311 | 'status': self.status, 312 | 'type': 'dns', 313 | 'token': 'evaGxfADs6pSRb2LAv9IZf17Dt3juxGJ-PCt92wr-oA', 314 | 'error': error, 315 | } 316 | self.jobj_from = self.jobj_to.copy() 317 | self.jobj_from['status'] = 'invalid' 318 | self.jobj_from['error'] = { 319 | 'type': 'urn:ietf:params:acme:error:serverInternal', 320 | 'detail': 'Unable to communicate with DNS server', 321 | } 322 | 323 | def test_encode(self): 324 | self.assertEqual(self.challb.encode('uri'), self.challb.uri) 325 | 326 | def test_to_partial_json(self): 327 | self.assertEqual(self.jobj_to, self.challb.to_partial_json()) 328 | 329 | def test_from_json(self): 330 | from acme.messages import ChallengeBody 331 | self.assertEqual(self.challb, ChallengeBody.from_json(self.jobj_from)) 332 | 333 | def test_from_json_hashable(self): 334 | from acme.messages import ChallengeBody 335 | hash(ChallengeBody.from_json(self.jobj_from)) 336 | 337 | def test_proxy(self): 338 | self.assertEqual(jose.b64decode( 339 | 'evaGxfADs6pSRb2LAv9IZf17Dt3juxGJ-PCt92wr-oA'), self.challb.token) 340 | 341 | 342 | class AuthorizationTest(unittest.TestCase): 343 | """Tests for acme.messages.Authorization.""" 344 | 345 | def setUp(self): 346 | from acme.messages import ChallengeBody 347 | from acme.messages import STATUS_VALID 348 | 349 | self.challbs = ( 350 | ChallengeBody( 351 | uri='http://challb1', status=STATUS_VALID, 352 | chall=challenges.HTTP01(token=b'IlirfxKKXAsHtmzK29Pj8A')), 353 | ChallengeBody(uri='http://challb2', status=STATUS_VALID, 354 | chall=challenges.DNS( 355 | token=b'DGyRejmCefe7v4NfDGDKfA')), 356 | ) 357 | combinations = ((0,), (1,)) 358 | 359 | from acme.messages import Authorization 360 | from acme.messages import Identifier 361 | from acme.messages import IDENTIFIER_FQDN 362 | identifier = Identifier(typ=IDENTIFIER_FQDN, value='example.com') 363 | self.authz = Authorization( 364 | identifier=identifier, combinations=combinations, 365 | challenges=self.challbs) 366 | 367 | self.jobj_from = { 368 | 'identifier': identifier.to_json(), 369 | 'challenges': [challb.to_json() for challb in self.challbs], 370 | 'combinations': combinations, 371 | } 372 | 373 | def test_from_json(self): 374 | from acme.messages import Authorization 375 | Authorization.from_json(self.jobj_from) 376 | 377 | def test_from_json_hashable(self): 378 | from acme.messages import Authorization 379 | hash(Authorization.from_json(self.jobj_from)) 380 | 381 | def test_resolved_combinations(self): 382 | self.assertEqual(self.authz.resolved_combinations, ( 383 | (self.challbs[0],), 384 | (self.challbs[1],), 385 | )) 386 | 387 | 388 | class AuthorizationResourceTest(unittest.TestCase): 389 | """Tests for acme.messages.AuthorizationResource.""" 390 | 391 | def test_json_de_serializable(self): 392 | from acme.messages import AuthorizationResource 393 | authzr = AuthorizationResource( 394 | uri=mock.sentinel.uri, 395 | body=mock.sentinel.body) 396 | self.assertTrue(isinstance(authzr, jose.JSONDeSerializable)) 397 | 398 | 399 | class CertificateRequestTest(unittest.TestCase): 400 | """Tests for acme.messages.CertificateRequest.""" 401 | 402 | def setUp(self): 403 | from acme.messages import CertificateRequest 404 | self.req = CertificateRequest(csr=CSR) 405 | 406 | def test_json_de_serializable(self): 407 | self.assertTrue(isinstance(self.req, jose.JSONDeSerializable)) 408 | from acme.messages import CertificateRequest 409 | self.assertEqual( 410 | self.req, CertificateRequest.from_json(self.req.to_json())) 411 | 412 | 413 | class CertificateResourceTest(unittest.TestCase): 414 | """Tests for acme.messages.CertificateResourceTest.""" 415 | 416 | def setUp(self): 417 | from acme.messages import CertificateResource 418 | self.certr = CertificateResource( 419 | body=CERT, uri=mock.sentinel.uri, authzrs=(), 420 | cert_chain_uri=mock.sentinel.cert_chain_uri) 421 | 422 | def test_json_de_serializable(self): 423 | self.assertTrue(isinstance(self.certr, jose.JSONDeSerializable)) 424 | from acme.messages import CertificateResource 425 | self.assertEqual( 426 | self.certr, CertificateResource.from_json(self.certr.to_json())) 427 | 428 | 429 | class RevocationTest(unittest.TestCase): 430 | """Tests for acme.messages.RevocationTest.""" 431 | 432 | def setUp(self): 433 | from acme.messages import Revocation 434 | self.rev = Revocation(certificate=CERT) 435 | 436 | def test_from_json_hashable(self): 437 | from acme.messages import Revocation 438 | hash(Revocation.from_json(self.rev.to_json())) 439 | 440 | 441 | class OrderResourceTest(unittest.TestCase): 442 | """Tests for acme.messages.OrderResource.""" 443 | 444 | def setUp(self): 445 | from acme.messages import OrderResource 446 | self.regr = OrderResource( 447 | body=mock.sentinel.body, uri=mock.sentinel.uri) 448 | 449 | def test_to_partial_json(self): 450 | self.assertEqual(self.regr.to_json(), { 451 | 'body': mock.sentinel.body, 452 | 'uri': mock.sentinel.uri, 453 | 'authorizations': None, 454 | }) 455 | 456 | class NewOrderTest(unittest.TestCase): 457 | """Tests for acme.messages.NewOrder.""" 458 | 459 | def setUp(self): 460 | from acme.messages import NewOrder 461 | self.reg = NewOrder( 462 | identifiers=mock.sentinel.identifiers) 463 | 464 | def test_to_partial_json(self): 465 | self.assertEqual(self.reg.to_json(), { 466 | 'identifiers': mock.sentinel.identifiers, 467 | }) 468 | 469 | 470 | if __name__ == '__main__': 471 | unittest.main() # pragma: no cover 472 | --------------------------------------------------------------------------------