././@PaxHeader0000000000000000000000000000003200000000000010210 xustar0026 mtime=1744063414.97455 acme-4.0.0/0000775000175000017500000000000014775045667011525 5ustar00willgwillg././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1744063413.0 acme-4.0.0/LICENSE.txt0000664000175000017500000002504214775045665013351 0ustar00willgwillg Copyright 2015 Electronic Frontier Foundation and others Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1744063413.0 acme-4.0.0/MANIFEST.in0000664000175000017500000000035414775045665013263 0ustar00willgwillginclude LICENSE.txt include README.rst include pytest.ini recursive-include docs * recursive-include examples * recursive-include acme/_internal/tests/testdata * include acme/py.typed global-exclude __pycache__ global-exclude *.py[cod] ././@PaxHeader0000000000000000000000000000003200000000000010210 xustar0026 mtime=1744063414.97455 acme-4.0.0/PKG-INFO0000644000175000017500000000275214775045667012626 0ustar00willgwillgMetadata-Version: 2.4 Name: acme Version: 4.0.0 Summary: ACME protocol implementation in Python Home-page: https://github.com/certbot/certbot Author: Certbot Project Author-email: certbot-dev@eff.org License: Apache License 2.0 Classifier: Development Status :: 5 - Production/Stable Classifier: Intended Audience :: Developers Classifier: License :: OSI Approved :: Apache Software License Classifier: Programming Language :: Python Classifier: Programming Language :: Python :: 3 Classifier: Programming Language :: Python :: 3.9 Classifier: Programming Language :: Python :: 3.10 Classifier: Programming Language :: Python :: 3.11 Classifier: Programming Language :: Python :: 3.12 Classifier: Programming Language :: Python :: 3.13 Classifier: Topic :: Internet :: WWW/HTTP Classifier: Topic :: Security Requires-Python: >=3.9 License-File: LICENSE.txt Requires-Dist: cryptography>=43.0.0 Requires-Dist: josepy>=2.0.0 Requires-Dist: PyOpenSSL>=25.0.0 Requires-Dist: pyrfc3339 Requires-Dist: pytz>=2019.3 Requires-Dist: requests>=2.20.0 Provides-Extra: docs Requires-Dist: Sphinx>=1.0; extra == "docs" Requires-Dist: sphinx_rtd_theme; extra == "docs" Provides-Extra: test Requires-Dist: pytest; extra == "test" Requires-Dist: pytest-xdist; extra == "test" Requires-Dist: typing-extensions; extra == "test" Dynamic: author Dynamic: author-email Dynamic: classifier Dynamic: home-page Dynamic: license Dynamic: license-file Dynamic: provides-extra Dynamic: requires-dist Dynamic: requires-python Dynamic: summary ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1744063413.0 acme-4.0.0/README.rst0000664000175000017500000000004714775045665013213 0ustar00willgwillgACME protocol implementation in Python ././@PaxHeader0000000000000000000000000000003200000000000010210 xustar0026 mtime=1744063414.96855 acme-4.0.0/acme/0000775000175000017500000000000014775045667012432 5ustar00willgwillg././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1744063413.0 acme-4.0.0/acme/__init__.py0000664000175000017500000000134014775045665014537 0ustar00willgwillg"""ACME protocol implementation. This module is an implementation of the `ACME protocol`_. .. _`ACME protocol`: https://datatracker.ietf.org/doc/html/rfc8555 """ import sys # This code exists to keep backwards compatibility with people using acme.jose # before it became the standalone josepy package. # # It is based on # https://github.com/requests/requests/blob/1278ecdf71a312dc2268f3bfc0aabfab3c006dcf/requests/packages.py import josepy as jose for mod in list(sys.modules): # This traversal is apparently necessary such that the identities are # preserved (acme.jose.* is josepy.*) if mod == 'josepy' or mod.startswith('josepy.'): sys.modules['acme.' + mod.replace('josepy', 'jose', 1)] = sys.modules[mod] ././@PaxHeader0000000000000000000000000000003200000000000010210 xustar0026 mtime=1744063414.96855 acme-4.0.0/acme/_internal/0000775000175000017500000000000014775045667014405 5ustar00willgwillg././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1744063413.0 acme-4.0.0/acme/_internal/__init__.py0000664000175000017500000000004514775045665016513 0ustar00willgwillg"""acme's internal implementation""" ././@PaxHeader0000000000000000000000000000003200000000000010210 xustar0026 mtime=1744063414.96955 acme-4.0.0/acme/_internal/tests/0000775000175000017500000000000014775045667015547 5ustar00willgwillg././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1744063413.0 acme-4.0.0/acme/_internal/tests/__init__.py0000664000175000017500000000002114775045665017647 0ustar00willgwillg"""acme tests""" ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1744063413.0 acme-4.0.0/acme/_internal/tests/challenges_test.py0000664000175000017500000004500614775045665021270 0ustar00willgwillg"""Tests for acme.challenges.""" import sys import unittest from unittest import mock import urllib.parse as urllib_parse import josepy as jose from josepy.jwk import JWKEC import OpenSSL import pytest import requests from acme import errors from acme._internal.tests import test_util CERT = test_util.load_cert('cert.pem') KEY = jose.JWKRSA(key=test_util.load_rsa_private_key('rsa512_key.pem')) class ChallengeTest(unittest.TestCase): def test_from_json_unrecognized(self): from acme.challenges import Challenge from acme.challenges import UnrecognizedChallenge chall = UnrecognizedChallenge({"type": "foo"}) assert chall == Challenge.from_json(chall.jobj) class UnrecognizedChallengeTest(unittest.TestCase): def setUp(self): from acme.challenges import UnrecognizedChallenge self.jobj = {"type": "foo"} self.chall = UnrecognizedChallenge(self.jobj) def test_to_partial_json(self): assert self.jobj == self.chall.to_partial_json() def test_from_json(self): from acme.challenges import UnrecognizedChallenge assert self.chall == UnrecognizedChallenge.from_json(self.jobj) class KeyAuthorizationChallengeResponseTest(unittest.TestCase): def setUp(self): def _encode(name): assert name == "token" return "foo" self.chall = mock.Mock() self.chall.encode.side_effect = _encode def test_verify_ok(self): from acme.challenges import KeyAuthorizationChallengeResponse response = KeyAuthorizationChallengeResponse( key_authorization='foo.oKGqedy-b-acd5eoybm2f-NVFxvyOoET5CNy3xnv8WY') assert response.verify(self.chall, KEY.public_key()) def test_verify_wrong_token(self): from acme.challenges import KeyAuthorizationChallengeResponse response = KeyAuthorizationChallengeResponse( key_authorization='bar.oKGqedy-b-acd5eoybm2f-NVFxvyOoET5CNy3xnv8WY') assert not response.verify(self.chall, KEY.public_key()) def test_verify_wrong_thumbprint(self): from acme.challenges import KeyAuthorizationChallengeResponse response = KeyAuthorizationChallengeResponse( key_authorization='foo.oKGqedy-b-acd5eoybm2f-NVFxv') assert not response.verify(self.chall, KEY.public_key()) def test_verify_wrong_form(self): from acme.challenges import KeyAuthorizationChallengeResponse response = KeyAuthorizationChallengeResponse( key_authorization='.foo.oKGqedy-b-acd5eoybm2f-' 'NVFxvyOoET5CNy3xnv8WY') assert not response.verify(self.chall, KEY.public_key()) class DNS01ResponseTest(unittest.TestCase): def setUp(self): from acme.challenges import DNS01Response self.msg = DNS01Response(key_authorization=u'foo') self.jmsg = { 'resource': 'challenge', 'type': 'dns-01', 'keyAuthorization': u'foo', } from acme.challenges import DNS01 self.chall = DNS01(token=(b'x' * 16)) self.response = self.chall.response(KEY) def test_to_partial_json(self): assert {} == self.msg.to_partial_json() def test_from_json(self): from acme.challenges import DNS01Response assert self.msg == DNS01Response.from_json(self.jmsg) def test_from_json_hashable(self): from acme.challenges import DNS01Response hash(DNS01Response.from_json(self.jmsg)) def test_simple_verify_failure(self): key2 = jose.JWKRSA.load(test_util.load_vector('rsa256_key.pem')) public_key = key2.public_key() verified = self.response.simple_verify(self.chall, "local", public_key) assert not verified def test_simple_verify_success(self): public_key = KEY.public_key() verified = self.response.simple_verify(self.chall, "local", public_key) assert verified class DNS01Test(unittest.TestCase): def setUp(self): from acme.challenges import DNS01 self.msg = DNS01(token=jose.decode_b64jose( 'evaGxfADs6pSRb2LAv9IZf17Dt3juxGJ+PCt92wr+oA')) self.jmsg = { 'type': 'dns-01', 'token': 'evaGxfADs6pSRb2LAv9IZf17Dt3juxGJ-PCt92wr-oA', } def test_validation_domain_name(self): assert '_acme-challenge.www.example.com' == \ self.msg.validation_domain_name('www.example.com') def test_validation(self): assert "rAa7iIg4K2y63fvUhCfy8dP1Xl7wEhmQq0oChTcE3Zk" == \ self.msg.validation(KEY) def test_to_partial_json(self): assert self.jmsg == self.msg.to_partial_json() def test_from_json(self): from acme.challenges import DNS01 assert self.msg == DNS01.from_json(self.jmsg) def test_from_json_hashable(self): from acme.challenges import DNS01 hash(DNS01.from_json(self.jmsg)) class HTTP01ResponseTest(unittest.TestCase): def setUp(self): from acme.challenges import HTTP01Response self.msg = HTTP01Response(key_authorization=u'foo') self.jmsg = { 'resource': 'challenge', 'type': 'http-01', 'keyAuthorization': u'foo', } from acme.challenges import HTTP01 self.chall = HTTP01(token=(b'x' * 16)) self.response = self.chall.response(KEY) def test_to_partial_json(self): assert {} == self.msg.to_partial_json() def test_from_json(self): from acme.challenges import HTTP01Response assert self.msg == HTTP01Response.from_json(self.jmsg) def test_from_json_hashable(self): from acme.challenges import HTTP01Response hash(HTTP01Response.from_json(self.jmsg)) def test_simple_verify_bad_key_authorization(self): key2 = jose.JWKRSA.load(test_util.load_vector('rsa256_key.pem')) self.response.simple_verify(self.chall, "local", key2.public_key()) @mock.patch("acme.challenges.requests.get") def test_simple_verify_good_validation(self, mock_get): validation = self.chall.validation(KEY) mock_get.return_value = mock.MagicMock(text=validation) assert self.response.simple_verify( self.chall, "local", KEY.public_key()) mock_get.assert_called_once_with(self.chall.uri("local"), verify=False, timeout=mock.ANY) @mock.patch("acme.challenges.requests.get") def test_simple_verify_bad_validation(self, mock_get): mock_get.return_value = mock.MagicMock(text="!") assert not self.response.simple_verify( self.chall, "local", KEY.public_key()) @mock.patch("acme.challenges.requests.get") def test_simple_verify_whitespace_validation(self, mock_get): from acme.challenges import HTTP01Response mock_get.return_value = mock.MagicMock( text=(self.chall.validation(KEY) + HTTP01Response.WHITESPACE_CUTSET)) assert self.response.simple_verify( self.chall, "local", KEY.public_key()) mock_get.assert_called_once_with(self.chall.uri("local"), verify=False, timeout=mock.ANY) @mock.patch("acme.challenges.requests.get") def test_simple_verify_connection_error(self, mock_get): mock_get.side_effect = requests.exceptions.RequestException assert not self.response.simple_verify( self.chall, "local", KEY.public_key()) @mock.patch("acme.challenges.requests.get") def test_simple_verify_port(self, mock_get): self.response.simple_verify( self.chall, domain="local", account_public_key=KEY.public_key(), port=8080) assert "local:8080" == urllib_parse.urlparse( mock_get.mock_calls[0][1][0]).netloc @mock.patch("acme.challenges.requests.get") def test_simple_verify_timeout(self, mock_get): self.response.simple_verify(self.chall, "local", KEY.public_key()) mock_get.assert_called_once_with(self.chall.uri("local"), verify=False, timeout=30) mock_get.reset_mock() self.response.simple_verify(self.chall, "local", KEY.public_key(), timeout=1234) mock_get.assert_called_once_with(self.chall.uri("local"), verify=False, timeout=1234) class HTTP01Test(unittest.TestCase): def setUp(self): from acme.challenges import HTTP01 self.msg = HTTP01( token=jose.decode_b64jose( 'evaGxfADs6pSRb2LAv9IZf17Dt3juxGJ+PCt92wr+oA')) self.jmsg = { 'type': 'http-01', 'token': 'evaGxfADs6pSRb2LAv9IZf17Dt3juxGJ-PCt92wr-oA', } def test_path(self): assert self.msg.path == '/.well-known/acme-challenge/' \ 'evaGxfADs6pSRb2LAv9IZf17Dt3juxGJ-PCt92wr-oA' def test_uri(self): assert 'http://example.com/.well-known/acme-challenge/' \ 'evaGxfADs6pSRb2LAv9IZf17Dt3juxGJ-PCt92wr-oA' == \ self.msg.uri('example.com') def test_to_partial_json(self): assert self.jmsg == self.msg.to_partial_json() def test_from_json(self): from acme.challenges import HTTP01 assert self.msg == HTTP01.from_json(self.jmsg) def test_from_json_hashable(self): from acme.challenges import HTTP01 hash(HTTP01.from_json(self.jmsg)) def test_good_token(self): assert self.msg.good_token assert not self.msg.update(token=b'..').good_token class TLSALPN01ResponseTest(unittest.TestCase): def setUp(self): from acme.challenges import TLSALPN01 self.chall = TLSALPN01( token=jose.b64decode(b'a82d5ff8ef740d12881f6d3c2277ab2e')) self.domain = u'example.com' self.domain2 = u'example2.com' self.response = self.chall.response(KEY) self.jmsg = { 'resource': 'challenge', 'type': 'tls-alpn-01', 'keyAuthorization': self.response.key_authorization, } def test_to_partial_json(self): assert {} == self.response.to_partial_json() def test_from_json(self): from acme.challenges import TLSALPN01Response assert self.response == TLSALPN01Response.from_json(self.jmsg) def test_from_json_hashable(self): from acme.challenges import TLSALPN01Response hash(TLSALPN01Response.from_json(self.jmsg)) def test_gen_verify_cert(self): key1 = test_util.load_pyopenssl_private_key('rsa512_key.pem') cert, key2 = self.response.gen_cert(self.domain, key1) assert key1 == key2 assert self.response.verify_cert(self.domain, cert) def test_gen_verify_cert_gen_key(self): cert, key = self.response.gen_cert(self.domain) assert isinstance(key, OpenSSL.crypto.PKey) assert self.response.verify_cert(self.domain, cert) def test_verify_bad_cert(self): assert not self.response.verify_cert(self.domain, test_util.load_cert('cert.pem')) def test_verify_bad_domain(self): key1 = test_util.load_pyopenssl_private_key('rsa512_key.pem') cert, key2 = self.response.gen_cert(self.domain, key1) assert key1 == key2 assert not self.response.verify_cert(self.domain2, cert) def test_simple_verify_bad_key_authorization(self): key2 = jose.JWKRSA.load(test_util.load_vector('rsa256_key.pem')) self.response.simple_verify(self.chall, "local", key2.public_key()) @mock.patch('acme.challenges.TLSALPN01Response.verify_cert', autospec=True) def test_simple_verify(self, mock_verify_cert): mock_verify_cert.return_value = mock.sentinel.verification assert mock.sentinel.verification == self.response.simple_verify( self.chall, self.domain, KEY.public_key(), cert=mock.sentinel.cert) mock_verify_cert.assert_called_once_with( self.response, self.domain, mock.sentinel.cert) @mock.patch('acme.challenges.socket.gethostbyname') @mock.patch('acme.challenges.crypto_util.probe_sni') def test_probe_cert(self, mock_probe_sni, mock_gethostbyname): mock_gethostbyname.return_value = '127.0.0.1' self.response.probe_cert('foo.com') mock_gethostbyname.assert_called_once_with('foo.com') mock_probe_sni.assert_called_once_with( host=b'127.0.0.1', port=self.response.PORT, name=b'foo.com', alpn_protocols=[b'acme-tls/1']) self.response.probe_cert('foo.com', host='8.8.8.8') mock_probe_sni.assert_called_with( host=b'8.8.8.8', port=mock.ANY, name=b'foo.com', alpn_protocols=[b'acme-tls/1']) @mock.patch('acme.challenges.TLSALPN01Response.probe_cert') def test_simple_verify_false_on_probe_error(self, mock_probe_cert): mock_probe_cert.side_effect = errors.Error assert not self.response.simple_verify( self.chall, self.domain, KEY.public_key()) class TLSALPN01Test(unittest.TestCase): def setUp(self): from acme.challenges import TLSALPN01 self.msg = TLSALPN01( token=jose.b64decode('a82d5ff8ef740d12881f6d3c2277ab2e')) self.jmsg = { 'type': 'tls-alpn-01', 'token': 'a82d5ff8ef740d12881f6d3c2277ab2e', } def test_to_partial_json(self): assert self.jmsg == self.msg.to_partial_json() def test_from_json(self): from acme.challenges import TLSALPN01 assert self.msg == TLSALPN01.from_json(self.jmsg) def test_from_json_hashable(self): from acme.challenges import TLSALPN01 hash(TLSALPN01.from_json(self.jmsg)) def test_from_json_invalid_token_length(self): from acme.challenges import TLSALPN01 self.jmsg['token'] = jose.encode_b64jose(b'abcd') with pytest.raises(jose.DeserializationError): TLSALPN01.from_json(self.jmsg) @mock.patch('acme.challenges.TLSALPN01Response.gen_cert') def test_validation(self, mock_gen_cert): mock_gen_cert.return_value = ('cert', 'key') assert ('cert', 'key') == self.msg.validation( KEY, cert_key=mock.sentinel.cert_key, domain=mock.sentinel.domain) mock_gen_cert.assert_called_once_with(key=mock.sentinel.cert_key, domain=mock.sentinel.domain) class DNSTest(unittest.TestCase): def setUp(self): from acme.challenges import DNS self.msg = DNS(token=jose.b64decode( b'evaGxfADs6pSRb2LAv9IZf17Dt3juxGJ-PCt92wr-oA')) self.jmsg = { 'type': 'dns', 'token': 'evaGxfADs6pSRb2LAv9IZf17Dt3juxGJ-PCt92wr-oA', } def test_to_partial_json(self): assert self.jmsg == self.msg.to_partial_json() def test_from_json(self): from acme.challenges import DNS assert self.msg == DNS.from_json(self.jmsg) def test_from_json_hashable(self): from acme.challenges import DNS hash(DNS.from_json(self.jmsg)) def test_gen_check_validation(self): ec_key_secp384r1 = JWKEC(key=test_util.load_ecdsa_private_key('ec_secp384r1_key.pem')) for key, alg in [(KEY, jose.RS256), (ec_key_secp384r1, jose.ES384)]: with self.subTest(key=key, alg=alg): assert self.msg.check_validation( self.msg.gen_validation(key, alg=alg), key.public_key()) def test_gen_check_validation_wrong_key(self): key2 = jose.JWKRSA.load(test_util.load_vector('rsa1024_key.pem')) assert not self.msg.check_validation( self.msg.gen_validation(KEY), key2.public_key()) def test_check_validation_wrong_payload(self): validations = tuple( jose.JWS.sign(payload=payload, alg=jose.RS256, key=KEY) for payload in (b'', b'{}') ) for validation in validations: assert not self.msg.check_validation( validation, KEY.public_key()) def test_check_validation_wrong_fields(self): bad_validation = jose.JWS.sign( payload=self.msg.update( token=b'x' * 20).json_dumps().encode('utf-8'), alg=jose.RS256, key=KEY) assert not self.msg.check_validation(bad_validation, KEY.public_key()) def test_gen_response(self): with mock.patch('acme.challenges.DNS.gen_validation') as mock_gen: mock_gen.return_value = mock.sentinel.validation response = self.msg.gen_response(KEY) from acme.challenges import DNSResponse assert isinstance(response, DNSResponse) assert response.validation == mock.sentinel.validation def test_validation_domain_name(self): assert '_acme-challenge.le.wtf' == self.msg.validation_domain_name('le.wtf') def test_validation_domain_name_ecdsa(self): ec_key_secp384r1 = JWKEC(key=test_util.load_ecdsa_private_key('ec_secp384r1_key.pem')) assert self.msg.check_validation( self.msg.gen_validation(ec_key_secp384r1, alg=jose.ES384), ec_key_secp384r1.public_key()) is True class DNSResponseTest(unittest.TestCase): def setUp(self): from acme.challenges import DNS self.chall = DNS(token=jose.b64decode( b"evaGxfADs6pSRb2LAv9IZf17Dt3juxGJ-PCt92wr-oA")) self.validation = jose.JWS.sign( payload=self.chall.json_dumps(sort_keys=True).encode(), key=KEY, alg=jose.RS256) from acme.challenges import DNSResponse self.msg = DNSResponse(validation=self.validation) self.jmsg_to = { 'validation': self.validation, } self.jmsg_from = { 'resource': 'challenge', 'type': 'dns', 'validation': self.validation.to_json(), } def test_to_partial_json(self): assert self.jmsg_to == self.msg.to_partial_json() def test_from_json(self): from acme.challenges import DNSResponse assert self.msg == DNSResponse.from_json(self.jmsg_from) def test_from_json_hashable(self): from acme.challenges import DNSResponse hash(DNSResponse.from_json(self.jmsg_from)) def test_check_validation(self): assert self.msg.check_validation(self.chall, KEY.public_key()) class JWSPayloadRFC8555Compliant(unittest.TestCase): """Test for RFC8555 compliance of JWS generated from resources/challenges""" def test_challenge_payload(self): from acme.challenges import HTTP01Response challenge_body = HTTP01Response() jobj = challenge_body.json_dumps(indent=2).encode() # RFC8555 states that challenge responses must have an empty payload. assert jobj == b'{}' if __name__ == '__main__': sys.exit(pytest.main(sys.argv[1:] + [__file__])) # pragma: no cover ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1744063413.0 acme-4.0.0/acme/_internal/tests/client_test.py0000664000175000017500000010476314775045665020447 0ustar00willgwillg"""Tests for acme.client.""" # pylint: disable=too-many-lines import copy import datetime import http.client as http_client import json import sys from typing import Dict import unittest from unittest import mock import josepy as jose import pytest import requests from acme import challenges from acme import errors from acme import jws as acme_jws from acme import messages from acme._internal.tests import messages_test from acme._internal.tests import test_util from acme.client import ClientNetwork from acme.client import ClientV2 CERT_SAN_PEM = test_util.load_vector('cert-san.pem') CSR_MIXED_PEM = test_util.load_vector('csr-mixed.pem') CSR_NO_SANS_PEM = test_util.load_vector('csr-nosans.pem') KEY = jose.JWKRSA.load(test_util.load_vector('rsa512_key.pem')) DIRECTORY_V2 = messages.Directory({ 'newAccount': 'https://www.letsencrypt-demo.org/acme/new-account', 'newNonce': 'https://www.letsencrypt-demo.org/acme/new-nonce', 'newOrder': 'https://www.letsencrypt-demo.org/acme/new-order', 'revokeCert': 'https://www.letsencrypt-demo.org/acme/revoke-cert', 'meta': messages.Directory.Meta(), }) class ClientV2Test(unittest.TestCase): """Tests for acme.client.ClientV2.""" def setUp(self): self.response = mock.MagicMock( ok=True, status_code=http_client.OK, headers={}, links={}) self.net = mock.MagicMock() self.net.post.return_value = self.response self.net.get.return_value = self.response self.identifier = messages.Identifier( typ=messages.IDENTIFIER_FQDN, value='example.com') # Registration self.contact = ('mailto:cert-admin@example.com', 'tel:+12025551212') reg = messages.Registration( contact=self.contact, key=KEY.public_key()) the_arg: Dict = dict(reg) self.new_reg = messages.NewRegistration(**the_arg) self.regr = messages.RegistrationResource( body=reg, uri='https://www.letsencrypt-demo.org/acme/reg/1') # Authorization authzr_uri = 'https://www.letsencrypt-demo.org/acme/authz/1' challb = messages.ChallengeBody( uri=(authzr_uri + '/1'), status=messages.STATUS_VALID, chall=challenges.DNS(token=jose.b64decode( 'evaGxfADs6pSRb2LAv9IZf17Dt3juxGJ-PCt92wr-oA'))) self.challr = messages.ChallengeResource( body=challb, authzr_uri=authzr_uri) self.authz = messages.Authorization( identifier=messages.Identifier( typ=messages.IDENTIFIER_FQDN, value='example.com'), challenges=(challb,)) self.authzr = messages.AuthorizationResource( body=self.authz, uri=authzr_uri) # Reason code for revocation self.rsn = 1 self.directory = DIRECTORY_V2 self.client = ClientV2(self.directory, self.net) self.new_reg = self.new_reg.update(terms_of_service_agreed=True) self.authzr_uri2 = 'https://www.letsencrypt-demo.org/acme/authz/2' self.authz2 = self.authz.update(identifier=messages.Identifier( typ=messages.IDENTIFIER_FQDN, value='www.example.com'), status=messages.STATUS_PENDING) self.authzr2 = messages.AuthorizationResource( body=self.authz2, uri=self.authzr_uri2) self.order = messages.Order( identifiers=(self.authz.identifier, self.authz2.identifier), status=messages.STATUS_PENDING, authorizations=(self.authzr.uri, self.authzr_uri2), finalize='https://www.letsencrypt-demo.org/acme/acct/1/order/1/finalize') self.orderr = messages.OrderResource( body=self.order, uri='https://www.letsencrypt-demo.org/acme/acct/1/order/1', authorizations=[self.authzr, self.authzr2], csr_pem=CSR_MIXED_PEM) self.orderr2 = messages.OrderResource( body=self.order, uri='https://www.letsencrypt-demo.org/acme/acct/1/order/1', authorizations=[self.authzr, self.authzr2], csr_pem=CSR_NO_SANS_PEM) def test_new_account(self): self.response.status_code = http_client.CREATED self.response.json.return_value = self.regr.body.to_json() self.response.headers['Location'] = self.regr.uri assert self.regr == self.client.new_account(self.new_reg) def test_new_account_tos_link(self): self.response.status_code = http_client.CREATED self.response.json.return_value = self.regr.body.to_json() self.response.headers['Location'] = self.regr.uri self.response.links.update({ 'terms-of-service': {'url': 'https://www.letsencrypt-demo.org/tos'}, }) assert self.client.new_account(self.new_reg).terms_of_service == \ 'https://www.letsencrypt-demo.org/tos' def test_new_account_conflict(self): self.response.status_code = http_client.OK self.response.headers['Location'] = self.regr.uri with pytest.raises(errors.ConflictError): self.client.new_account(self.new_reg) def test_deactivate_account(self): deactivated_regr = self.regr.update( body=self.regr.body.update(status='deactivated')) self.response.json.return_value = deactivated_regr.body.to_json() self.response.status_code = http_client.OK self.response.headers['Location'] = self.regr.uri assert self.client.deactivate_registration(self.regr) == deactivated_regr def test_deactivate_authorization(self): deactivated_authz = self.authzr.update( body=self.authzr.body.update(status=messages.STATUS_DEACTIVATED)) self.response.json.return_value = deactivated_authz.body.to_json() authzr = self.client.deactivate_authorization(self.authzr) assert deactivated_authz.body == authzr.body assert self.client.net.post.call_count == 1 assert self.authzr.uri in self.net.post.call_args_list[0][0] def test_new_order(self): order_response = copy.deepcopy(self.response) order_response.status_code = http_client.CREATED order_response.json.return_value = self.order.to_json() order_response.headers['Location'] = self.orderr.uri self.net.post.return_value = order_response authz_response = copy.deepcopy(self.response) authz_response.json.return_value = self.authz.to_json() authz_response.headers['Location'] = self.authzr.uri authz_response2 = self.response authz_response2.json.return_value = self.authz2.to_json() authz_response2.headers['Location'] = self.authzr2.uri with mock.patch('acme.client.ClientV2._post_as_get') as mock_post_as_get: mock_post_as_get.side_effect = (authz_response, authz_response2) assert self.client.new_order(CSR_MIXED_PEM) == self.orderr with mock.patch('acme.client.ClientV2._post_as_get') as mock_post_as_get: mock_post_as_get.side_effect = (authz_response, authz_response2) assert self.client.new_order(CSR_NO_SANS_PEM) == self.orderr2 def test_answer_challege(self): self.response.links['up'] = {'url': self.challr.authzr_uri} self.response.json.return_value = self.challr.body.to_json() chall_response = challenges.DNSResponse(validation=None) self.client.answer_challenge(self.challr.body, chall_response) with pytest.raises(errors.UnexpectedUpdate): self.client.answer_challenge(self.challr.body.update(uri='foo'), chall_response) def test_answer_challenge_missing_next(self): with pytest.raises(errors.ClientError): self.client.answer_challenge(self.challr.body, challenges.DNSResponse(validation=None)) @mock.patch('acme.client.datetime') def test_poll_and_finalize(self, mock_datetime): mock_datetime.datetime.now.return_value = datetime.datetime(2018, 2, 15) mock_datetime.timedelta = datetime.timedelta expected_deadline = mock_datetime.datetime.now() + datetime.timedelta(seconds=90) self.client.poll_authorizations = mock.Mock(return_value=self.orderr) self.client.finalize_order = mock.Mock(return_value=self.orderr) assert self.client.poll_and_finalize(self.orderr) == self.orderr self.client.poll_authorizations.assert_called_once_with(self.orderr, expected_deadline) self.client.finalize_order.assert_called_once_with(self.orderr, expected_deadline) @mock.patch('acme.client.datetime') def test_poll_authorizations_timeout(self, mock_datetime): now_side_effect = [datetime.datetime(2018, 2, 15), datetime.datetime(2018, 2, 16), datetime.datetime(2018, 2, 17)] mock_datetime.datetime.now.side_effect = now_side_effect self.response.json.side_effect = [ self.authz.to_json(), self.authz2.to_json(), self.authz2.to_json()] with pytest.raises(errors.TimeoutError): self.client.poll_authorizations(self.orderr, now_side_effect[1]) def test_poll_authorizations_failure(self): deadline = datetime.datetime(9999, 9, 9) challb = self.challr.body.update(status=messages.STATUS_INVALID, error=messages.Error.with_code('unauthorized')) authz = self.authz.update(status=messages.STATUS_INVALID, challenges=(challb,)) self.response.json.return_value = authz.to_json() with pytest.raises(errors.ValidationError): self.client.poll_authorizations(self.orderr, deadline) def test_poll_authorizations_success(self): deadline = datetime.datetime(9999, 9, 9) updated_authz2 = self.authz2.update(status=messages.STATUS_VALID) updated_authzr2 = messages.AuthorizationResource( body=updated_authz2, uri=self.authzr_uri2) updated_orderr = self.orderr.update(authorizations=[self.authzr, updated_authzr2]) self.response.json.side_effect = ( self.authz.to_json(), self.authz2.to_json(), updated_authz2.to_json()) assert self.client.poll_authorizations(self.orderr, deadline) == updated_orderr def test_poll_unexpected_update(self): updated_authz = self.authz.update(identifier=self.identifier.update(value='foo')) self.response.json.return_value = updated_authz.to_json() with pytest.raises(errors.UnexpectedUpdate): self.client.poll(self.authzr) def test_finalize_order_success(self): updated_order = self.order.update( certificate='https://www.letsencrypt-demo.org/acme/cert/', status=messages.STATUS_VALID) updated_orderr = self.orderr.update(body=updated_order, fullchain_pem=CERT_SAN_PEM) self.response.json.return_value = updated_order.to_json() self.response.text = CERT_SAN_PEM deadline = datetime.datetime(9999, 9, 9) assert self.client.finalize_order(self.orderr, deadline) == updated_orderr def test_finalize_order_error(self): updated_order = self.order.update( error=messages.Error.with_code('unauthorized'), status=messages.STATUS_INVALID) self.response.json.return_value = updated_order.to_json() deadline = datetime.datetime(9999, 9, 9) with pytest.raises(errors.IssuanceError): self.client.finalize_order(self.orderr, deadline) def test_finalize_order_invalid_status(self): # https://github.com/certbot/certbot/issues/9296 order = self.order.update(error=None, status=messages.STATUS_INVALID) self.response.json.return_value = order.to_json() with pytest.raises(errors.Error, match="The certificate order failed"): self.client.finalize_order(self.orderr, datetime.datetime(9999, 9, 9)) def test_finalize_order_timeout(self): deadline = datetime.datetime.now() - datetime.timedelta(seconds=60) with pytest.raises(errors.TimeoutError): self.client.finalize_order(self.orderr, deadline) def test_finalize_order_alt_chains(self): updated_order = self.order.update( certificate='https://www.letsencrypt-demo.org/acme/cert/', status=messages.STATUS_VALID ) updated_orderr = self.orderr.update(body=updated_order, fullchain_pem=CERT_SAN_PEM, alternative_fullchains_pem=[CERT_SAN_PEM, CERT_SAN_PEM]) self.response.json.return_value = updated_order.to_json() self.response.text = CERT_SAN_PEM self.response.headers['Link'] =';rel="alternate", ' + \ ';rel="index", ' + \ ';title="foo";rel="alternate"' deadline = datetime.datetime(9999, 9, 9) resp = self.client.finalize_order(self.orderr, deadline, fetch_alternative_chains=True) self.net.post.assert_any_call('https://example.com/acme/cert/1', mock.ANY, new_nonce_url=mock.ANY) self.net.post.assert_any_call('https://example.com/acme/cert/2', mock.ANY, new_nonce_url=mock.ANY) assert resp == updated_orderr del self.response.headers['Link'] resp = self.client.finalize_order(self.orderr, deadline, fetch_alternative_chains=True) assert resp == updated_orderr.update(alternative_fullchains_pem=[]) def test_revoke(self): self.client.revoke(messages_test.CERT, self.rsn) self.net.post.assert_called_once_with( self.directory["revokeCert"], mock.ANY, new_nonce_url=DIRECTORY_V2['newNonce']) def test_revoke_bad_status_raises_error(self): self.response.status_code = http_client.METHOD_NOT_ALLOWED with pytest.raises(errors.ClientError): self.client.revoke(messages_test.CERT, self.rsn) def test_update_registration(self): # "Instance of 'Field' has no to_json/update member" bug: self.response.headers['Location'] = self.regr.uri self.response.json.return_value = self.regr.body.to_json() assert self.regr == self.client.update_registration(self.regr) assert self.client.net.account is not None assert self.client.net.post.call_count == 2 assert DIRECTORY_V2.newAccount in self.net.post.call_args_list[0][0] self.response.json.return_value = self.regr.body.update( contact=()).to_json() def test_external_account_required_true(self): self.client.directory = messages.Directory({ 'meta': messages.Directory.Meta(external_account_required=True) }) assert self.client.external_account_required() def test_external_account_required_false(self): self.client.directory = messages.Directory({ 'meta': messages.Directory.Meta(external_account_required=False) }) assert not self.client.external_account_required() def test_external_account_required_default(self): assert not self.client.external_account_required() def test_query_registration_client(self): self.response.json.return_value = self.regr.body.to_json() self.response.headers['Location'] = 'https://www.letsencrypt-demo.org/acme/reg/1' assert self.regr == self.client.query_registration(self.regr) def test_post_as_get(self): with mock.patch('acme.client.ClientV2._authzr_from_response') as mock_client: mock_client.return_value = self.authzr2 self.client.poll(self.authzr2) # pylint: disable=protected-access self.client.net.post.assert_called_once_with( self.authzr2.uri, None, new_nonce_url='https://www.letsencrypt-demo.org/acme/new-nonce') self.client.net.get.assert_not_called() def test_retry_after_date(self): self.response.headers['Retry-After'] = 'Fri, 31 Dec 1999 23:59:59 GMT' assert datetime.datetime(1999, 12, 31, 23, 59, 59) == \ self.client.retry_after(response=self.response, default=10) @mock.patch('acme.client.datetime') def test_retry_after_invalid(self, dt_mock): dt_mock.datetime.now.return_value = datetime.datetime(2015, 3, 27) dt_mock.timedelta = datetime.timedelta self.response.headers['Retry-After'] = 'foooo' assert datetime.datetime(2015, 3, 27, 0, 0, 10) == \ self.client.retry_after(response=self.response, default=10) @mock.patch('acme.client.datetime') def test_retry_after_overflow(self, dt_mock): dt_mock.datetime.now.return_value = datetime.datetime(2015, 3, 27) dt_mock.timedelta = datetime.timedelta dt_mock.datetime.side_effect = datetime.datetime self.response.headers['Retry-After'] = "Tue, 116 Feb 2016 11:50:00 MST" assert datetime.datetime(2015, 3, 27, 0, 0, 10) == \ self.client.retry_after(response=self.response, default=10) @mock.patch('acme.client.datetime') def test_retry_after_seconds(self, dt_mock): dt_mock.datetime.now.return_value = datetime.datetime(2015, 3, 27) dt_mock.timedelta = datetime.timedelta self.response.headers['Retry-After'] = '50' assert datetime.datetime(2015, 3, 27, 0, 0, 50) == \ self.client.retry_after(response=self.response, default=10) @mock.patch('acme.client.datetime') def test_retry_after_missing(self, dt_mock): dt_mock.datetime.now.return_value = datetime.datetime(2015, 3, 27) dt_mock.timedelta = datetime.timedelta assert datetime.datetime(2015, 3, 27, 0, 0, 10) == \ self.client.retry_after(response=self.response, default=10) def test_get_directory(self): self.response.json.return_value = DIRECTORY_V2.to_json() assert DIRECTORY_V2.to_partial_json() == \ ClientV2.get_directory('https://example.com/dir', self.net).to_partial_json() class MockJSONDeSerializable(jose.JSONDeSerializable): # pylint: disable=missing-docstring def __init__(self, value): self.value = value def to_partial_json(self): return {'foo': self.value} @classmethod def from_json(cls, jobj): pass # pragma: no cover class ClientNetworkTest(unittest.TestCase): """Tests for acme.client.ClientNetwork.""" def setUp(self): self.verify_ssl = mock.MagicMock() self.wrap_in_jws = mock.MagicMock(return_value=mock.sentinel.wrapped) self.net = ClientNetwork( key=KEY, alg=jose.RS256, verify_ssl=self.verify_ssl, user_agent='acme-python-test') self.response = mock.MagicMock(ok=True, status_code=http_client.OK) self.response.headers = {} self.response.links = {} def test_init(self): assert self.net.verify_ssl is self.verify_ssl def test_wrap_in_jws(self): # pylint: disable=protected-access jws_dump = self.net._wrap_in_jws( MockJSONDeSerializable('foo'), nonce=b'Tg', url="url") jws = acme_jws.JWS.json_loads(jws_dump) assert json.loads(jws.payload.decode()) == {'foo': 'foo'} assert jws.signature.combined.nonce == b'Tg' def test_wrap_in_jws_v2(self): self.net.account = {'uri': 'acct-uri'} # pylint: disable=protected-access jws_dump = self.net._wrap_in_jws( MockJSONDeSerializable('foo'), nonce=b'Tg', url="url") jws = acme_jws.JWS.json_loads(jws_dump) assert json.loads(jws.payload.decode()) == {'foo': 'foo'} assert jws.signature.combined.nonce == b'Tg' assert jws.signature.combined.kid == u'acct-uri' assert jws.signature.combined.url == u'url' def test_check_response_not_ok_jobj_no_error(self): self.response.ok = False self.response.json.return_value = {} with mock.patch('acme.client.messages.Error.from_json') as from_json: from_json.side_effect = jose.DeserializationError # pylint: disable=protected-access with pytest.raises(errors.ClientError): self.net._check_response(self.response) def test_check_response_not_ok_jobj_error(self): self.response.ok = False self.response.json.return_value = messages.Error.with_code( 'serverInternal', detail='foo', title='some title').to_json() # pylint: disable=protected-access with pytest.raises(messages.Error): self.net._check_response(self.response) def test_check_response_not_ok_no_jobj(self): self.response.ok = False self.response.json.side_effect = ValueError # pylint: disable=protected-access with pytest.raises(errors.ClientError): self.net._check_response(self.response) def test_check_response_ok_no_jobj_ct_required(self): self.response.json.side_effect = ValueError for response_ct in [self.net.JSON_CONTENT_TYPE, 'foo']: self.response.headers['Content-Type'] = response_ct # pylint: disable=protected-access with pytest.raises(errors.ClientError): self.net._check_response(self.response, content_type=self.net.JSON_CONTENT_TYPE) def test_check_response_ok_no_jobj_no_ct(self): self.response.json.side_effect = ValueError for response_ct in [self.net.JSON_CONTENT_TYPE, 'foo']: self.response.headers['Content-Type'] = response_ct # pylint: disable=protected-access assert self.response == self.net._check_response(self.response) @mock.patch('acme.client.logger') def test_check_response_ok_ct_with_charset(self, mock_logger): self.response.json.return_value = {} self.response.headers['Content-Type'] = 'application/json; charset=utf-8' # pylint: disable=protected-access assert self.response == self.net._check_response( self.response, content_type='application/json') try: mock_logger.debug.assert_called_with( 'Ignoring wrong Content-Type (%r) for JSON decodable response', 'application/json; charset=utf-8' ) except AssertionError: return raise AssertionError('Expected Content-Type warning ' #pragma: no cover 'to not have been logged') @mock.patch('acme.client.logger') def test_check_response_ok_bad_ct(self, mock_logger): self.response.json.return_value = {} self.response.headers['Content-Type'] = 'text/plain' # pylint: disable=protected-access assert self.response == self.net._check_response( self.response, content_type='application/json') mock_logger.debug.assert_called_with( 'Ignoring wrong Content-Type (%r) for JSON decodable response', 'text/plain' ) def test_check_response_conflict(self): self.response.ok = False self.response.status_code = 409 # pylint: disable=protected-access with pytest.raises(errors.ConflictError): self.net._check_response(self.response) def test_check_response_jobj(self): self.response.json.return_value = {} for response_ct in [self.net.JSON_CONTENT_TYPE, 'foo']: self.response.headers['Content-Type'] = response_ct # pylint: disable=protected-access assert self.response == self.net._check_response(self.response) def test_send_request(self): self.net.session = mock.MagicMock() self.net.session.request.return_value = self.response # pylint: disable=protected-access assert self.response == self.net._send_request( 'HEAD', 'http://example.com/', 'foo', bar='baz') self.net.session.request.assert_called_once_with( 'HEAD', 'http://example.com/', 'foo', headers=mock.ANY, verify=mock.ANY, timeout=mock.ANY, bar='baz') @mock.patch('acme.client.logger') def test_send_request_get_der(self, mock_logger): self.net.session = mock.MagicMock() self.net.session.request.return_value = mock.MagicMock( ok=True, status_code=http_client.OK, content=b"hi") # pylint: disable=protected-access self.net._send_request('HEAD', 'http://example.com/', 'foo', timeout=mock.ANY, bar='baz', headers={'Accept': 'application/pkix-cert'}) mock_logger.debug.assert_called_with( 'Received response:\nHTTP %d\n%s\n\n%s', 200, '', b'aGk=') def test_send_request_post(self): self.net.session = mock.MagicMock() self.net.session.request.return_value = self.response # pylint: disable=protected-access assert self.response == self.net._send_request( 'POST', 'http://example.com/', 'foo', data='qux', bar='baz') self.net.session.request.assert_called_once_with( 'POST', 'http://example.com/', 'foo', headers=mock.ANY, verify=mock.ANY, timeout=mock.ANY, data='qux', bar='baz') def test_send_request_verify_ssl(self): # pylint: disable=protected-access for verify in True, False: self.net.session = mock.MagicMock() self.net.session.request.return_value = self.response self.net.verify_ssl = verify # pylint: disable=protected-access assert self.response == \ self.net._send_request('GET', 'http://example.com/') self.net.session.request.assert_called_once_with( 'GET', 'http://example.com/', verify=verify, timeout=mock.ANY, headers=mock.ANY) def test_send_request_user_agent(self): self.net.session = mock.MagicMock() # pylint: disable=protected-access self.net._send_request('GET', 'http://example.com/', headers={'bar': 'baz'}) self.net.session.request.assert_called_once_with( 'GET', 'http://example.com/', verify=mock.ANY, timeout=mock.ANY, headers={'User-Agent': 'acme-python-test', 'bar': 'baz'}) self.net._send_request('GET', 'http://example.com/', headers={'User-Agent': 'foo2'}) self.net.session.request.assert_called_with( 'GET', 'http://example.com/', verify=mock.ANY, timeout=mock.ANY, headers={'User-Agent': 'foo2'}) def test_send_request_timeout(self): self.net.session = mock.MagicMock() # pylint: disable=protected-access self.net._send_request('GET', 'http://example.com/', headers={'bar': 'baz'}) self.net.session.request.assert_called_once_with( mock.ANY, mock.ANY, verify=mock.ANY, headers=mock.ANY, timeout=45) def test_del(self, close_exception=None): sess = mock.MagicMock() if close_exception is not None: sess.close.side_effect = close_exception self.net.session = sess del self.net sess.close.assert_called_once_with() def test_del_error(self): self.test_del(ReferenceError) @mock.patch('acme.client.requests') def test_requests_error_passthrough(self, mock_requests): mock_requests.exceptions = requests.exceptions mock_requests.request.side_effect = requests.exceptions.RequestException # pylint: disable=protected-access with pytest.raises(requests.exceptions.RequestException): self.net._send_request('GET', 'uri') def test_urllib_error(self): # Using a connection error to test a properly formatted error message try: # pylint: disable=protected-access self.net._send_request('GET', "http://localhost:19123/nonexistent.txt") # Value Error Generated Exceptions except ValueError as y: assert "Requesting localhost/nonexistent: " \ "Connection refused" == str(y) # Requests Library Exceptions except requests.exceptions.ConnectionError as z: #pragma: no cover assert "'Connection aborted.'" in str(z) or "[WinError 10061]" in str(z) class ClientNetworkWithMockedResponseTest(unittest.TestCase): """Tests for acme.client.ClientNetwork which mock out response.""" def setUp(self): self.net = ClientNetwork(key=None, alg=None) self.response = mock.MagicMock(ok=True, status_code=http_client.OK) self.response.headers = {} self.response.links = {} self.response.checked = False self.acmev1_nonce_response = mock.MagicMock( ok=False, status_code=http_client.METHOD_NOT_ALLOWED) self.acmev1_nonce_response.headers = {} self.obj = mock.MagicMock() self.wrapped_obj = mock.MagicMock() self.content_type = mock.sentinel.content_type self.all_nonces = [ jose.b64encode(b'Nonce'), jose.b64encode(b'Nonce2'), jose.b64encode(b'Nonce3')] self.available_nonces = self.all_nonces[:] def send_request(*args, **kwargs): # pylint: disable=unused-argument,missing-docstring assert "new_nonce_url" not in kwargs method = args[0] uri = args[1] if method == 'HEAD' and uri != "new_nonce_uri": response = self.acmev1_nonce_response else: response = self.response if self.available_nonces: response.headers = { self.net.REPLAY_NONCE_HEADER: self.available_nonces.pop().decode()} else: response.headers = {} return response # pylint: disable=protected-access self.net._send_request = self.send_request = mock.MagicMock( side_effect=send_request) self.net._check_response = self.check_response self.net._wrap_in_jws = mock.MagicMock(return_value=self.wrapped_obj) def check_response(self, response, content_type): # pylint: disable=missing-docstring assert self.response == response assert self.content_type == content_type assert self.response.ok self.response.checked = True return self.response def test_head(self): assert self.acmev1_nonce_response == self.net.head( 'http://example.com/', 'foo', bar='baz') self.send_request.assert_called_once_with( 'HEAD', 'http://example.com/', 'foo', bar='baz') def test_head_v2(self): assert self.response == self.net.head( 'new_nonce_uri', 'foo', bar='baz') self.send_request.assert_called_once_with( 'HEAD', 'new_nonce_uri', 'foo', bar='baz') def test_get(self): assert self.response == self.net.get( 'http://example.com/', content_type=self.content_type, bar='baz') assert self.response.checked self.send_request.assert_called_once_with( 'GET', 'http://example.com/', bar='baz') def test_post_no_content_type(self): self.content_type = self.net.JOSE_CONTENT_TYPE assert self.response == self.net.post('uri', self.obj) assert self.response.checked def test_post(self): # pylint: disable=protected-access assert self.response == self.net.post( 'uri', self.obj, content_type=self.content_type) assert self.response.checked self.net._wrap_in_jws.assert_called_once_with( self.obj, jose.b64decode(self.all_nonces.pop()), "uri") self.available_nonces = [] with pytest.raises(errors.MissingNonce): self.net.post('uri', self.obj, content_type=self.content_type) self.net._wrap_in_jws.assert_called_with( self.obj, jose.b64decode(self.all_nonces.pop()), "uri") def test_post_wrong_initial_nonce(self): # HEAD self.available_nonces = [b'f', jose.b64encode(b'good')] with pytest.raises(errors.BadNonce): self.net.post('uri', self.obj, content_type=self.content_type) def test_post_wrong_post_response_nonce(self): self.available_nonces = [jose.b64encode(b'good'), b'f'] with pytest.raises(errors.BadNonce): self.net.post('uri', self.obj, content_type=self.content_type) def test_post_failed_retry(self): check_response = mock.MagicMock() check_response.side_effect = messages.Error.with_code('badNonce') # pylint: disable=protected-access self.net._check_response = check_response with pytest.raises(messages.Error): self.net.post('uri', self.obj, content_type=self.content_type) def test_post_not_retried(self): check_response = mock.MagicMock() check_response.side_effect = [messages.Error.with_code('malformed'), self.response] # pylint: disable=protected-access self.net._check_response = check_response with pytest.raises(messages.Error): self.net.post('uri', self.obj, content_type=self.content_type) def test_post_successful_retry(self): post_once = mock.MagicMock() post_once.side_effect = [messages.Error.with_code('badNonce'), self.response] # pylint: disable=protected-access assert self.response == self.net.post( 'uri', self.obj, content_type=self.content_type) def test_head_get_post_error_passthrough(self): self.send_request.side_effect = requests.exceptions.RequestException for method in self.net.head, self.net.get: with pytest.raises(requests.exceptions.RequestException): method('GET', 'uri') with pytest.raises(requests.exceptions.RequestException): self.net.post('uri', obj=self.obj) def test_post_bad_nonce_head(self): # pylint: disable=protected-access # regression test for https://github.com/certbot/certbot/issues/6092 bad_response = mock.MagicMock(ok=False, status_code=http_client.SERVICE_UNAVAILABLE) self.net._send_request = mock.MagicMock() self.net._send_request.return_value = bad_response self.content_type = None check_response = mock.MagicMock() self.net._check_response = check_response with pytest.raises(errors.ClientError): self.net.post('uri', self.obj, content_type=self.content_type, new_nonce_url='new_nonce_uri') assert check_response.call_count == 1 def test_new_nonce_uri_removed(self): self.content_type = None self.net.post('uri', self.obj, content_type=None, new_nonce_url='new_nonce_uri') if __name__ == '__main__': sys.exit(pytest.main(sys.argv[1:] + [__file__])) # pragma: no cover ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1744063413.0 acme-4.0.0/acme/_internal/tests/crypto_util_test.py0000664000175000017500000002746514775045665021551 0ustar00willgwillg"""Tests for acme.crypto_util.""" import ipaddress import itertools import socket import socketserver import sys import threading import time from typing import List import unittest from unittest import mock import warnings import pytest from cryptography import x509 from cryptography.hazmat.primitives import serialization from cryptography.hazmat.primitives.asymmetric import rsa, x25519 from acme import errors from acme._internal.tests import test_util class FormatTest(unittest.TestCase): def test_to_cryptography_encoding(self): from acme.crypto_util import Format assert Format.DER.to_cryptography_encoding() == serialization.Encoding.DER assert Format.PEM.to_cryptography_encoding() == serialization.Encoding.PEM class SSLSocketAndProbeSNITest(unittest.TestCase): """Tests for acme.crypto_util.SSLSocket/probe_sni.""" def setUp(self): self.cert = test_util.load_cert('rsa2048_cert.pem') key = test_util.load_pyopenssl_private_key('rsa2048_key.pem') # pylint: disable=protected-access certs = {b'foo': (key, self.cert)} from acme.crypto_util import SSLSocket class _TestServer(socketserver.TCPServer): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.socket = SSLSocket(self.socket, certs) self.server = _TestServer(('', 0), socketserver.BaseRequestHandler) self.port = self.server.socket.getsockname()[1] self.server_thread = threading.Thread( target=self.server.handle_request) def tearDown(self): if self.server_thread.is_alive(): # The thread may have already terminated. self.server_thread.join() # pragma: no cover self.server.server_close() def _probe(self, name): from acme.crypto_util import probe_sni return probe_sni(name, host='127.0.0.1', port=self.port) def _start_server(self): self.server_thread.start() time.sleep(1) # TODO: avoid race conditions in other way def test_probe_ok(self): self._start_server() assert self.cert == self._probe(b'foo') def test_probe_not_recognized_name(self): self._start_server() with pytest.raises(errors.Error): self._probe(b'bar') def test_probe_connection_error(self): self.server.server_close() original_timeout = socket.getdefaulttimeout() try: socket.setdefaulttimeout(1) with pytest.raises(errors.Error): self._probe(b'bar') finally: socket.setdefaulttimeout(original_timeout) class SSLSocketTest(unittest.TestCase): """Tests for acme.crypto_util.SSLSocket.""" def test_ssl_socket_invalid_arguments(self): from acme.crypto_util import SSLSocket with pytest.raises(ValueError): _ = SSLSocket(None, {'sni': ('key', 'cert')}, cert_selection=lambda _: None) with pytest.raises(ValueError): _ = SSLSocket(None) class MiscTests(unittest.TestCase): def test_dump_cryptography_chain(self): from acme.crypto_util import dump_cryptography_chain cert1 = test_util.load_cert('rsa2048_cert.pem') cert2 = test_util.load_cert('rsa4096_cert.pem') chain = [cert1, cert2] dumped = dump_cryptography_chain(chain) # default is PEM encoding Encoding.PEM assert isinstance(dumped, bytes) class CryptographyCertOrReqSANTest(unittest.TestCase): """Test for acme.crypto_util._cryptography_cert_or_req_san.""" @classmethod def _call(cls, loader, name): # pylint: disable=protected-access from acme.crypto_util import _cryptography_cert_or_req_san return _cryptography_cert_or_req_san(loader(name)) @classmethod def _get_idn_names(cls): """Returns expected names from '{cert,csr}-idnsans.pem'.""" chars = [chr(i) for i in itertools.chain(range(0x3c3, 0x400), range(0x641, 0x6fc), range(0x1820, 0x1877))] return [''.join(chars[i: i + 45]) + '.invalid' for i in range(0, len(chars), 45)] def _call_cert(self, name): return self._call(test_util.load_cert, name) def _call_csr(self, name): return self._call(test_util.load_csr, name) def test_cert_no_sans(self): assert self._call_cert('cert.pem') == [] def test_cert_two_sans(self): assert self._call_cert('cert-san.pem') == \ ['example.com', 'www.example.com'] def test_cert_hundred_sans(self): assert self._call_cert('cert-100sans.pem') == \ ['example{0}.com'.format(i) for i in range(1, 101)] def test_cert_idn_sans(self): assert self._call_cert('cert-idnsans.pem') == \ self._get_idn_names() def test_csr_no_sans(self): assert self._call_csr('csr-nosans.pem') == [] def test_csr_one_san(self): assert self._call_csr('csr.pem') == ['example.com'] def test_csr_two_sans(self): assert self._call_csr('csr-san.pem') == \ ['example.com', 'www.example.com'] def test_csr_six_sans(self): assert self._call_csr('csr-6sans.pem') == \ ['example.com', 'example.org', 'example.net', 'example.info', 'subdomain.example.com', 'other.subdomain.example.com'] def test_csr_hundred_sans(self): assert self._call_csr('csr-100sans.pem') == \ ['example{0}.com'.format(i) for i in range(1, 101)] def test_csr_idn_sans(self): assert self._call_csr('csr-idnsans.pem') == \ self._get_idn_names() def test_critical_san(self): assert self._call_cert('critical-san.pem') == \ ['chicago-cubs.venafi.example', 'cubs.venafi.example'] class GenMakeSelfSignedCertTest(unittest.TestCase): """Test for make_self_signed_cert.""" def setUp(self): self.cert_count = 5 self.serial_num: List[int] = [] self.privkey = rsa.generate_private_key(public_exponent=65537, key_size=2048) def test_sn_collisions(self): from acme.crypto_util import make_self_signed_cert for _ in range(self.cert_count): cert = make_self_signed_cert(self.privkey, ['dummy'], force_san=True, ips=[ipaddress.ip_address("10.10.10.10")]) self.serial_num.append(cert.serial_number) assert len(set(self.serial_num)) >= self.cert_count def test_no_ips(self): from acme.crypto_util import make_self_signed_cert cert = make_self_signed_cert(self.privkey, ['dummy']) @mock.patch("acme.crypto_util._now") def test_expiry_times(self, mock_now): from acme.crypto_util import make_self_signed_cert from datetime import datetime, timedelta, timezone not_before = 1736200830 validity = 100 not_before_dt = datetime.fromtimestamp(not_before) validity_td = timedelta(validity) not_after_dt = not_before_dt + validity_td cert = make_self_signed_cert( self.privkey, ['dummy'], not_before=not_before_dt, validity=validity_td, ) # TODO: This should be `not_valid_before_utc` once we raise the minimum # cryptography version. # https://github.com/certbot/certbot/issues/10105 with warnings.catch_warnings(): warnings.filterwarnings( 'ignore', message='Properties that return.*datetime object' ) self.assertEqual(cert.not_valid_before, not_before_dt) self.assertEqual(cert.not_valid_after, not_after_dt) now = not_before + 1 now_dt = datetime.fromtimestamp(now) mock_now.return_value = now_dt.replace(tzinfo=timezone.utc) valid_after_now_dt = now_dt + validity_td cert = make_self_signed_cert( self.privkey, ['dummy'], validity=validity_td, ) with warnings.catch_warnings(): warnings.filterwarnings( 'ignore', message='Properties that return.*datetime object' ) self.assertEqual(cert.not_valid_before, now_dt) self.assertEqual(cert.not_valid_after, valid_after_now_dt) def test_no_name(self): from acme.crypto_util import make_self_signed_cert with pytest.raises(AssertionError): make_self_signed_cert(self.privkey, ips=[ipaddress.ip_address("1.1.1.1")]) make_self_signed_cert(self.privkey) def test_extensions(self): from acme.crypto_util import make_self_signed_cert extension_type = x509.TLSFeature([x509.TLSFeatureType.status_request]) extension = x509.Extension( x509.TLSFeature.oid, False, extension_type ) cert = make_self_signed_cert( self.privkey, ips=[ipaddress.ip_address("1.1.1.1")], extensions=[extension] ) self.assertIn(extension, cert.extensions) class MakeCSRTest(unittest.TestCase): """Test for standalone functions.""" @classmethod def _call_with_key(cls, *args, **kwargs): privkey = rsa.generate_private_key(public_exponent=65537, key_size=2048) privkey_pem = privkey.private_bytes( serialization.Encoding.PEM, serialization.PrivateFormat.PKCS8, serialization.NoEncryption(), ) from acme.crypto_util import make_csr return make_csr(privkey_pem, *args, **kwargs) def test_make_csr(self): csr_pem = self._call_with_key(["a.example", "b.example"]) assert b"--BEGIN CERTIFICATE REQUEST--" in csr_pem assert b"--END CERTIFICATE REQUEST--" in csr_pem csr = x509.load_pem_x509_csr(csr_pem) assert len(csr.extensions) == 1 assert list( csr.extensions.get_extension_for_class(x509.SubjectAlternativeName).value ) == [ x509.DNSName("a.example"), x509.DNSName("b.example"), ] def test_make_csr_ip(self): csr_pem = self._call_with_key( ["a.example"], False, [ipaddress.ip_address("127.0.0.1"), ipaddress.ip_address("::1")], ) assert b"--BEGIN CERTIFICATE REQUEST--" in csr_pem assert b"--END CERTIFICATE REQUEST--" in csr_pem csr = x509.load_pem_x509_csr(csr_pem) assert len(csr.extensions) == 1 assert list( csr.extensions.get_extension_for_class(x509.SubjectAlternativeName).value ) == [ x509.DNSName("a.example"), x509.IPAddress(ipaddress.ip_address("127.0.0.1")), x509.IPAddress(ipaddress.ip_address("::1")), ] def test_make_csr_must_staple(self): csr_pem = self._call_with_key(["a.example"], must_staple=True) csr = x509.load_pem_x509_csr(csr_pem) assert len(csr.extensions) == 2 assert list(csr.extensions.get_extension_for_class(x509.TLSFeature).value) == [ x509.TLSFeatureType.status_request ] def test_make_csr_without_hostname(self): with pytest.raises(ValueError): self._call_with_key() def test_make_csr_invalid_key_type(self): privkey = x25519.X25519PrivateKey.generate() privkey_pem = privkey.private_bytes( serialization.Encoding.PEM, serialization.PrivateFormat.PKCS8, serialization.NoEncryption(), ) from acme.crypto_util import make_csr with pytest.raises(ValueError): make_csr(privkey_pem, ["a.example"]) if __name__ == '__main__': sys.exit(pytest.main(sys.argv[1:] + [__file__])) # pragma: no cover ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1744063413.0 acme-4.0.0/acme/_internal/tests/errors_test.py0000664000175000017500000000277614775045665020506 0ustar00willgwillg"""Tests for acme.errors.""" import sys import unittest from unittest import mock import pytest class BadNonceTest(unittest.TestCase): """Tests for acme.errors.BadNonce.""" def setUp(self): from acme.errors import BadNonce self.error = BadNonce(nonce="xxx", error="error") def test_str(self): assert "Invalid nonce ('xxx'): error" == str(self.error) class MissingNonceTest(unittest.TestCase): """Tests for acme.errors.MissingNonce.""" def setUp(self): from acme.errors import MissingNonce self.response = mock.MagicMock(headers={}) self.response.request.method = 'FOO' self.error = MissingNonce(self.response) def test_str(self): assert "FOO" in str(self.error) assert "{}" in str(self.error) class PollErrorTest(unittest.TestCase): """Tests for acme.errors.PollError.""" def setUp(self): from acme.errors import PollError self.timeout = PollError( exhausted={mock.sentinel.AR}, updated={}) self.invalid = PollError(exhausted=set(), updated={ mock.sentinel.AR: mock.sentinel.AR2}) def test_timeout(self): assert self.timeout.timeout assert not self.invalid.timeout def test_repr(self): assert 'PollError(exhausted=%s, updated={sentinel.AR: ' \ 'sentinel.AR2})' % repr(set()) == repr(self.invalid) if __name__ == "__main__": sys.exit(pytest.main(sys.argv[1:] + [__file__])) # pragma: no cover ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1744063413.0 acme-4.0.0/acme/_internal/tests/fields_test.py0000664000175000017500000000326714775045665020434 0ustar00willgwillg"""Tests for acme.fields.""" import datetime import sys import unittest import warnings import josepy as jose import pytest import pytz class FixedTest(unittest.TestCase): """Tests for acme.fields.Fixed.""" def setUp(self): from acme.fields import fixed self.field = fixed('name', 'x') def test_decode(self): assert 'x' == self.field.decode('x') def test_decode_bad(self): with pytest.raises(jose.DeserializationError): self.field.decode('y') def test_encode(self): assert 'x' == self.field.encode('x') def test_encode_override(self): assert 'y' == self.field.encode('y') class RFC3339FieldTest(unittest.TestCase): """Tests for acme.fields.RFC3339Field.""" def setUp(self): self.decoded = datetime.datetime(2015, 3, 27, tzinfo=pytz.UTC) self.encoded = '2015-03-27T00:00:00Z' def test_default_encoder(self): from acme.fields import RFC3339Field assert self.encoded == RFC3339Field.default_encoder(self.decoded) def test_default_encoder_naive_fails(self): from acme.fields import RFC3339Field with pytest.raises(ValueError): RFC3339Field.default_encoder(datetime.datetime.now()) def test_default_decoder(self): from acme.fields import RFC3339Field assert self.decoded == RFC3339Field.default_decoder(self.encoded) def test_default_decoder_raises_deserialization_error(self): from acme.fields import RFC3339Field with pytest.raises(jose.DeserializationError): RFC3339Field.default_decoder('') if __name__ == '__main__': sys.exit(pytest.main(sys.argv[1:] + [__file__])) # pragma: no cover ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1744063413.0 acme-4.0.0/acme/_internal/tests/jose_test.py0000664000175000017500000000343714775045665020125 0ustar00willgwillg"""Tests for acme.jose shim.""" import importlib import sys import pytest def _test_it(submodule, attribute): if submodule: acme_jose_path = 'acme.jose.' + submodule josepy_path = 'josepy.' + submodule else: acme_jose_path = 'acme.jose' josepy_path = 'josepy' acme_jose_mod = importlib.import_module(acme_jose_path) josepy_mod = importlib.import_module(josepy_path) assert acme_jose_mod is josepy_mod assert getattr(acme_jose_mod, attribute) is getattr(josepy_mod, attribute) # We use the imports below with eval, but pylint doesn't # understand that. import josepy # pylint: disable=unused-import import acme # pylint: disable=unused-import acme_jose_mod = eval(acme_jose_path) # pylint: disable=eval-used josepy_mod = eval(josepy_path) # pylint: disable=eval-used assert acme_jose_mod is josepy_mod assert getattr(acme_jose_mod, attribute) is getattr(josepy_mod, attribute) def test_top_level(): _test_it('', 'RS512') def test_submodules(): # This test ensures that the modules in josepy that were # available at the time it was moved into its own package are # available under acme.jose. Backwards compatibility with new # modules or testing code is not maintained. mods_and_attrs = [('b64', 'b64decode',), ('errors', 'Error',), ('interfaces', 'JSONDeSerializable',), ('json_util', 'Field',), ('jwa', 'HS256',), ('jwk', 'JWK',), ('jws', 'JWS',), ('util', 'ImmutableMap',),] for mod, attr in mods_and_attrs: _test_it(mod, attr) if __name__ == '__main__': sys.exit(pytest.main(sys.argv[1:] + [__file__])) # pragma: no cover ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1744063413.0 acme-4.0.0/acme/_internal/tests/jws_test.py0000664000175000017500000000412014775045665017756 0ustar00willgwillg"""Tests for acme.jws.""" import sys import unittest import josepy as jose import pytest from acme._internal.tests import test_util KEY = jose.JWKRSA.load(test_util.load_vector('rsa512_key.pem')) class HeaderTest(unittest.TestCase): """Tests for acme.jws.Header.""" good_nonce = jose.encode_b64jose(b'foo') wrong_nonce = u'F' # Following just makes sure wrong_nonce is wrong try: jose.b64decode(wrong_nonce) except (ValueError, TypeError): assert True else: pytest.fail("Exception from jose.b64decode wasn't raised") # pragma: no cover def test_nonce_decoder(self): from acme.jws import Header nonce_field = Header._fields['nonce'] with pytest.raises(jose.DeserializationError): nonce_field.decode(self.wrong_nonce) assert b'foo' == nonce_field.decode(self.good_nonce) class JWSTest(unittest.TestCase): """Tests for acme.jws.JWS.""" def setUp(self): self.privkey = KEY self.pubkey = self.privkey.public_key() self.nonce = jose.b64encode(b'Nonce') self.url = 'hi' self.kid = 'baaaaa' def test_kid_serialize(self): from acme.jws import JWS jws = JWS.sign(payload=b'foo', key=self.privkey, alg=jose.RS256, nonce=self.nonce, url=self.url, kid=self.kid) assert jws.signature.combined.nonce == self.nonce assert jws.signature.combined.url == self.url assert jws.signature.combined.kid == self.kid assert jws.signature.combined.jwk is None # TODO: check that nonce is in protected header assert jws == JWS.from_json(jws.to_json()) def test_jwk_serialize(self): from acme.jws import JWS jws = JWS.sign(payload=b'foo', key=self.privkey, alg=jose.RS256, nonce=self.nonce, url=self.url) assert jws.signature.combined.kid is None assert jws.signature.combined.jwk == self.pubkey if __name__ == '__main__': sys.exit(pytest.main(sys.argv[1:] + [__file__])) # pragma: no cover ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1744063413.0 acme-4.0.0/acme/_internal/tests/messages_test.py0000664000175000017500000004761714775045665021004 0ustar00willgwillg"""Tests for acme.messages.""" import sys from typing import Dict import unittest from unittest import mock import josepy as jose import pytest from acme import challenges from acme._internal.tests import test_util CERT = test_util.load_cert('cert.der') CSR = test_util.load_csr('csr.der') KEY = test_util.load_rsa_private_key('rsa512_key.pem') class ErrorTest(unittest.TestCase): """Tests for acme.messages.Error.""" def setUp(self): from acme.messages import Error from acme.messages import ERROR_PREFIX from acme.messages import Identifier from acme.messages import IDENTIFIER_FQDN self.error = Error.with_code('malformed', detail='foo', title='title') self.jobj = { 'detail': 'foo', 'title': 'some title', 'type': ERROR_PREFIX + 'malformed', } self.error_custom = Error(typ='custom', detail='bar') self.identifier = Identifier(typ=IDENTIFIER_FQDN, value='example.com') self.subproblem = Error.with_code('caa', detail='bar', title='title', identifier=self.identifier) self.error_with_subproblems = Error.with_code('malformed', detail='foo', title='title', subproblems=[self.subproblem]) self.empty_error = Error() def test_default_typ(self): from acme.messages import Error assert Error().typ == 'about:blank' def test_from_json_empty(self): from acme.messages import Error assert Error() == Error.from_json('{}') def test_from_json_hashable(self): from acme.messages import Error hash(Error.from_json(self.error.to_json())) def test_from_json_with_subproblems(self): from acme.messages import Error parsed_error = Error.from_json(self.error_with_subproblems.to_json()) assert 1 == len(parsed_error.subproblems) assert self.subproblem == parsed_error.subproblems[0] def test_description(self): assert 'The request message was malformed' == self.error.description assert self.error_custom.description is None def test_code(self): from acme.messages import Error assert 'malformed' == self.error.code assert self.error_custom.code is None assert Error().code is None def test_is_acme_error(self): from acme.messages import Error from acme.messages import is_acme_error assert is_acme_error(self.error) assert not is_acme_error(self.error_custom) assert not is_acme_error(Error()) assert not is_acme_error(self.empty_error) assert not is_acme_error("must pet all the {dogs|rabbits}") def test_unicode_error(self): from acme.messages import Error from acme.messages import is_acme_error arabic_error = Error.with_code( 'malformed', detail=u'\u0639\u062f\u0627\u0644\u0629', title='title') assert is_acme_error(arabic_error) def test_with_code(self): from acme.messages import Error from acme.messages import is_acme_error assert is_acme_error(Error.with_code('badCSR')) with pytest.raises(ValueError): Error.with_code('not an ACME error code') def test_str(self): assert str(self.error) == \ u"{0.typ} :: {0.description} :: {0.detail} :: {0.title}" \ .format(self.error) assert str(self.error_with_subproblems) == \ (u"{0.typ} :: {0.description} :: {0.detail} :: {0.title}\n"+ u"Problem for {1.identifier.value}: {1.typ} :: {1.description} :: {1.detail} :: {1.title}").format( self.error_with_subproblems, self.subproblem) # this test is based on a minimal reproduction of a contextmanager/immutable # exception related error: https://github.com/python/cpython/issues/99856 def test_setting_traceback(self): assert self.error_custom.__traceback__ is None try: 1/0 except ZeroDivisionError as e: self.error_custom.__traceback__ = e.__traceback__ assert self.error_custom.__traceback__ is not None class ConstantTest(unittest.TestCase): """Tests for acme.messages._Constant.""" def setUp(self): from acme.messages import _Constant class MockConstant(_Constant): # pylint: disable=missing-docstring POSSIBLE_NAMES: Dict = {} self.MockConstant = MockConstant # pylint: disable=invalid-name self.const_a = MockConstant('a') self.const_b = MockConstant('b') def test_to_partial_json(self): assert 'a' == self.const_a.to_partial_json() assert 'b' == self.const_b.to_partial_json() def test_from_json(self): assert self.const_a == self.MockConstant.from_json('a') with pytest.raises(jose.DeserializationError): self.MockConstant.from_json('c') def test_from_json_hashable(self): hash(self.MockConstant.from_json('a')) def test_repr(self): assert 'MockConstant(a)' == repr(self.const_a) assert 'MockConstant(b)' == repr(self.const_b) def test_equality(self): const_a_prime = self.MockConstant('a') assert self.const_a != self.const_b assert self.const_a == const_a_prime assert self.const_a != self.const_b assert self.const_a == const_a_prime class DirectoryTest(unittest.TestCase): """Tests for acme.messages.Directory.""" def setUp(self): from acme.messages import Directory self.dir = Directory({ 'newReg': 'reg', 'newCert': 'cert', 'meta': Directory.Meta( terms_of_service='https://example.com/acme/terms', website='https://www.example.com/', caa_identities=['example.com'], profiles={ "example": "some profile", "other example": "a different profile" } ), }) def test_init_wrong_key_value_success(self): # pylint: disable=no-self-use from acme.messages import Directory Directory({'foo': 'bar'}) def test_getitem(self): assert 'reg' == self.dir['newReg'] def test_getitem_fails_with_key_error(self): with pytest.raises(KeyError): self.dir.__getitem__('foo') def test_getattr(self): assert 'reg' == self.dir.newReg def test_getattr_fails_with_attribute_error(self): with pytest.raises(AttributeError): self.dir.__getattr__('foo') def test_to_json(self): assert self.dir.to_json() == { 'newReg': 'reg', 'newCert': 'cert', 'meta': { 'termsOfService': 'https://example.com/acme/terms', 'website': 'https://www.example.com/', 'caaIdentities': ['example.com'], 'profiles': { 'example': 'some profile', 'other example': 'a different profile' } }, } def test_from_json_deserialization_unknown_key_success(self): # pylint: disable=no-self-use from acme.messages import Directory Directory.from_json({'foo': 'bar'}) def test_iter_meta(self): result = False for k in self.dir.meta: if k == 'terms_of_service': result = self.dir.meta[k] == 'https://example.com/acme/terms' assert result class ExternalAccountBindingTest(unittest.TestCase): def setUp(self): from acme.messages import Directory self.key = jose.jwk.JWKRSA(key=KEY.public_key()) self.kid = "kid-for-testing" self.hmac_key = "hmac-key-for-testing" self.dir = Directory({ 'newAccount': 'http://url/acme/new-account', }) def test_from_data(self): from acme.messages import ExternalAccountBinding eab = ExternalAccountBinding.from_data(self.key, self.kid, self.hmac_key, self.dir) assert len(eab) == 3 assert sorted(eab.keys()) == sorted(['protected', 'payload', 'signature']) class RegistrationTest(unittest.TestCase): """Tests for acme.messages.Registration.""" def setUp(self): key = jose.jwk.JWKRSA(key=KEY.public_key()) contact = ( 'mailto:admin@foo.com', 'tel:1234', ) agreement = 'https://letsencrypt.org/terms' from acme.messages import Registration self.reg = Registration(key=key, contact=contact, agreement=agreement) self.reg_none = Registration() self.jobj_to = { 'contact': contact, 'agreement': agreement, 'key': key, } self.jobj_from = self.jobj_to.copy() self.jobj_from['key'] = key.to_json() def test_from_data(self): from acme.messages import Registration reg = Registration.from_data(phone='1234', email='admin@foo.com') assert reg.contact == ( 'tel:1234', 'mailto:admin@foo.com', ) def test_new_registration_from_data_with_eab(self): from acme.messages import Directory from acme.messages import ExternalAccountBinding from acme.messages import NewRegistration key = jose.jwk.JWKRSA(key=KEY.public_key()) kid = "kid-for-testing" hmac_key = "hmac-key-for-testing" directory = Directory({ 'newAccount': 'http://url/acme/new-account', }) eab = ExternalAccountBinding.from_data(key, kid, hmac_key, directory) reg = NewRegistration.from_data(email='admin@foo.com', external_account_binding=eab) assert reg.contact == ( 'mailto:admin@foo.com', ) assert sorted(reg.external_account_binding.keys()) == \ sorted(['protected', 'payload', 'signature']) def test_phones(self): assert ('1234',) == self.reg.phones def test_emails(self): assert ('admin@foo.com',) == self.reg.emails def test_to_partial_json(self): assert self.jobj_to == self.reg.to_partial_json() def test_from_json(self): from acme.messages import Registration assert self.reg == Registration.from_json(self.jobj_from) def test_from_json_hashable(self): from acme.messages import Registration hash(Registration.from_json(self.jobj_from)) def test_default_not_transmitted(self): from acme.messages import NewRegistration empty_new_reg = NewRegistration() new_reg_with_contact = NewRegistration(contact=()) assert empty_new_reg.contact == () assert new_reg_with_contact.contact == () assert 'contact' not in empty_new_reg.to_partial_json() assert 'contact' not in empty_new_reg.fields_to_partial_json() assert 'contact' in new_reg_with_contact.to_partial_json() assert 'contact' in new_reg_with_contact.fields_to_partial_json() class UpdateRegistrationTest(unittest.TestCase): """Tests for acme.messages.UpdateRegistration.""" def test_empty(self): from acme.messages import UpdateRegistration jstring = '{"resource": "reg"}' assert '{}' == UpdateRegistration().json_dumps() assert UpdateRegistration() == UpdateRegistration.json_loads(jstring) class RegistrationResourceTest(unittest.TestCase): """Tests for acme.messages.RegistrationResource.""" def setUp(self): from acme.messages import RegistrationResource self.regr = RegistrationResource( body=mock.sentinel.body, uri=mock.sentinel.uri, terms_of_service=mock.sentinel.terms_of_service) def test_to_partial_json(self): assert self.regr.to_json() == { 'body': mock.sentinel.body, 'uri': mock.sentinel.uri, 'terms_of_service': mock.sentinel.terms_of_service, } class ChallengeResourceTest(unittest.TestCase): """Tests for acme.messages.ChallengeResource.""" def test_uri(self): from acme.messages import ChallengeResource assert 'http://challb' == ChallengeResource(body=mock.MagicMock( uri='http://challb'), authzr_uri='http://authz').uri class ChallengeBodyTest(unittest.TestCase): """Tests for acme.messages.ChallengeBody.""" def setUp(self): self.chall = challenges.DNS(token=jose.b64decode( 'evaGxfADs6pSRb2LAv9IZf17Dt3juxGJ-PCt92wr-oA')) from acme.messages import ChallengeBody from acme.messages import Error from acme.messages import STATUS_INVALID self.status = STATUS_INVALID error = Error.with_code('serverInternal', detail='Unable to communicate with DNS server') self.challb = ChallengeBody( uri='http://challb', chall=self.chall, status=self.status, error=error) self.jobj_to = { 'url': 'http://challb', 'status': self.status, 'type': 'dns', 'token': 'evaGxfADs6pSRb2LAv9IZf17Dt3juxGJ-PCt92wr-oA', 'error': error, } self.jobj_from = self.jobj_to.copy() self.jobj_from['status'] = 'invalid' self.jobj_from['error'] = { 'type': 'urn:ietf:params:acme:error:serverInternal', 'detail': 'Unable to communicate with DNS server', } def test_encode(self): assert self.challb.encode('uri') == self.challb.uri def test_to_partial_json(self): assert self.jobj_to == self.challb.to_partial_json() def test_from_json(self): from acme.messages import ChallengeBody assert self.challb == ChallengeBody.from_json(self.jobj_from) def test_from_json_hashable(self): from acme.messages import ChallengeBody hash(ChallengeBody.from_json(self.jobj_from)) def test_proxy(self): assert jose.b64decode( 'evaGxfADs6pSRb2LAv9IZf17Dt3juxGJ-PCt92wr-oA') == self.challb.token class AuthorizationTest(unittest.TestCase): """Tests for acme.messages.Authorization.""" def setUp(self): from acme.messages import ChallengeBody from acme.messages import STATUS_VALID self.challbs = ( ChallengeBody( uri='http://challb1', status=STATUS_VALID, chall=challenges.HTTP01(token=b'IlirfxKKXAsHtmzK29Pj8A')), ChallengeBody(uri='http://challb2', status=STATUS_VALID, chall=challenges.DNS( token=b'DGyRejmCefe7v4NfDGDKfA')), ) from acme.messages import Authorization from acme.messages import Identifier from acme.messages import IDENTIFIER_FQDN identifier = Identifier(typ=IDENTIFIER_FQDN, value='example.com') self.authz = Authorization( identifier=identifier, challenges=self.challbs) self.jobj_from = { 'identifier': identifier.to_json(), 'challenges': [challb.to_json() for challb in self.challbs], } def test_from_json(self): from acme.messages import Authorization Authorization.from_json(self.jobj_from) def test_from_json_hashable(self): from acme.messages import Authorization hash(Authorization.from_json(self.jobj_from)) class AuthorizationResourceTest(unittest.TestCase): """Tests for acme.messages.AuthorizationResource.""" def test_json_de_serializable(self): from acme.messages import AuthorizationResource authzr = AuthorizationResource( uri=mock.sentinel.uri, body=mock.sentinel.body) assert isinstance(authzr, jose.JSONDeSerializable) class CertificateRequestTest(unittest.TestCase): """Tests for acme.messages.CertificateRequest.""" def setUp(self): from acme.messages import CertificateRequest self.req = CertificateRequest(csr=CSR) def test_json_de_serializable(self): assert isinstance(self.req, jose.JSONDeSerializable) from acme.messages import CertificateRequest assert self.req == CertificateRequest.from_json(self.req.to_json()) class CertificateResourceTest(unittest.TestCase): """Tests for acme.messages.CertificateResourceTest.""" def setUp(self): from acme.messages import CertificateResource self.certr = CertificateResource( body=CERT, uri=mock.sentinel.uri, authzrs=(), cert_chain_uri=mock.sentinel.cert_chain_uri) def test_json_de_serializable(self): assert isinstance(self.certr, jose.JSONDeSerializable) from acme.messages import CertificateResource assert self.certr == CertificateResource.from_json(self.certr.to_json()) class RevocationTest(unittest.TestCase): """Tests for acme.messages.RevocationTest.""" def setUp(self): from acme.messages import Revocation self.rev = Revocation(certificate=CERT) def test_from_json_hashable(self): from acme.messages import Revocation hash(Revocation.from_json(self.rev.to_json())) class OrderResourceTest(unittest.TestCase): """Tests for acme.messages.OrderResource.""" def setUp(self): from acme.messages import OrderResource self.regr = OrderResource( body=mock.sentinel.body, uri=mock.sentinel.uri) def test_to_partial_json(self): assert self.regr.to_json() == { 'body': mock.sentinel.body, 'uri': mock.sentinel.uri, 'authorizations': None, } def test_json_de_serializable(self): from acme.messages import ChallengeBody from acme.messages import STATUS_PENDING challbs = ( ChallengeBody( uri='http://challb1', status=STATUS_PENDING, chall=challenges.HTTP01(token=b'IlirfxKKXAsHtmzK29Pj8A')), ChallengeBody(uri='http://challb2', status=STATUS_PENDING, chall=challenges.DNS( token=b'DGyRejmCefe7v4NfDGDKfA')), ) from acme.messages import Authorization from acme.messages import AuthorizationResource from acme.messages import Identifier from acme.messages import IDENTIFIER_FQDN identifier = Identifier(typ=IDENTIFIER_FQDN, value='example.com') authz = AuthorizationResource(uri="http://authz1", body=Authorization( identifier=identifier, challenges=challbs)) from acme.messages import Order body = Order(identifiers=(identifier,), status=STATUS_PENDING, authorizations=tuple(challb.uri for challb in challbs)) from acme.messages import OrderResource orderr = OrderResource(uri="http://order1", body=body, csr_pem=b'test blob', authorizations=(authz,)) self.assertEqual(orderr, OrderResource.from_json(orderr.to_json())) class NewOrderTest(unittest.TestCase): """Tests for acme.messages.NewOrder.""" def setUp(self): from acme.messages import NewOrder self.order = NewOrder( identifiers=mock.sentinel.identifiers) def test_to_partial_json(self): assert self.order.to_json() == { 'identifiers': mock.sentinel.identifiers, } def test_default_profile_empty(self): assert self.order.profile is None def test_non_empty_profile(self): from acme.messages import NewOrder order = NewOrder(identifiers=mock.sentinel.identifiers, profile='example') assert order.to_json() == { 'identifiers': mock.sentinel.identifiers, 'profile': 'example', } class JWSPayloadRFC8555Compliant(unittest.TestCase): """Test for RFC8555 compliance of JWS generated from resources/challenges""" def test_message_payload(self): from acme.messages import NewAuthorization new_order = NewAuthorization() jobj = new_order.json_dumps(indent=2).encode() # RFC8555 states that JWS bodies must not have a resource field. assert jobj == b'{}' if __name__ == '__main__': sys.exit(pytest.main(sys.argv[1:] + [__file__])) # pragma: no cover ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1744063413.0 acme-4.0.0/acme/_internal/tests/standalone_test.py0000664000175000017500000002350014775045665021306 0ustar00willgwillg"""Tests for acme.standalone.""" import http.client as http_client import socket import socketserver import sys import threading from typing import Set import unittest from unittest import mock import josepy as jose import pytest import requests from cryptography import x509 from cryptography.hazmat.primitives import serialization from acme import challenges from acme import crypto_util from acme import errors from acme._internal.tests import test_util class TLSServerTest(unittest.TestCase): """Tests for acme.standalone.TLSServer.""" def test_bind(self): # pylint: disable=no-self-use from acme.standalone import TLSServer server = TLSServer( ('', 0), socketserver.BaseRequestHandler, bind_and_activate=True) server.server_close() def test_ipv6(self): if socket.has_ipv6: from acme.standalone import TLSServer server = TLSServer( ('', 0), socketserver.BaseRequestHandler, bind_and_activate=True, ipv6=True) server.server_close() class HTTP01ServerTest(unittest.TestCase): """Tests for acme.standalone.HTTP01Server.""" def setUp(self): self.account_key = jose.JWK.load( test_util.load_vector('rsa1024_key.pem')) self.resources: Set = set() from acme.standalone import HTTP01Server self.server = HTTP01Server(('', 0), resources=self.resources) self.port = self.server.socket.getsockname()[1] self.thread = threading.Thread(target=self.server.serve_forever) self.thread.start() def tearDown(self): self.server.shutdown() self.thread.join() self.server.server_close() def test_index(self): response = requests.get( 'http://localhost:{0}'.format(self.port), verify=False) assert response.text == 'ACME client standalone challenge solver' assert response.ok def test_404(self): response = requests.get( 'http://localhost:{0}/foo'.format(self.port), verify=False) assert response.status_code == http_client.NOT_FOUND def _test_http01(self, add): chall = challenges.HTTP01(token=(b'x' * 16)) response, validation = chall.response_and_validation(self.account_key) from acme.standalone import HTTP01RequestHandler resource = HTTP01RequestHandler.HTTP01Resource( chall=chall, response=response, validation=validation) if add: self.resources.add(resource) return resource.response.simple_verify( resource.chall, 'localhost', self.account_key.public_key(), port=self.port) def test_http01_found(self): assert self._test_http01(add=True) def test_http01_not_found(self): assert not self._test_http01(add=False) def test_timely_shutdown(self): from acme.standalone import HTTP01Server with HTTP01Server(('', 0), resources=set(), timeout=0.05) as server: server_thread = threading.Thread(target=server.serve_forever) server_thread.start() with socket.socket() as client: client.connect(('localhost', server.socket.getsockname()[1])) stop_thread = threading.Thread(target=server.shutdown) stop_thread.start() server_thread.join(5.) is_hung = server_thread.is_alive() try: client.shutdown(socket.SHUT_RDWR) except: # pragma: no cover, pylint: disable=bare-except # may raise error because socket could already be closed pass assert not is_hung, 'Server shutdown should not be hung' @unittest.skipIf(not challenges.TLSALPN01.is_supported(), "pyOpenSSL too old") class TLSALPN01ServerTest(unittest.TestCase): """Test for acme.standalone.TLSALPN01Server.""" def setUp(self): self.certs = {b'localhost': ( serialization.load_pem_private_key(test_util.load_vector('rsa2048_key.pem'), password=None), x509.load_pem_x509_certificate(test_util.load_vector('rsa2048_cert.pem')), )} # Use different certificate for challenge. self.challenge_certs = {b'localhost': ( serialization.load_pem_private_key(test_util.load_vector('rsa4096_key.pem'), password=None), x509.load_pem_x509_certificate(test_util.load_vector('rsa4096_cert.pem')), )} from acme.standalone import TLSALPN01Server self.server = TLSALPN01Server(("localhost", 0), certs=self.certs, challenge_certs=self.challenge_certs) # pylint: disable=no-member self.thread = threading.Thread(target=self.server.serve_forever) self.thread.start() def tearDown(self): self.server.shutdown() # pylint: disable=no-member self.thread.join() self.server.server_close() # TODO: This is not implemented yet, see comments in standalone.py # def test_certs(self): # host, port = self.server.socket.getsockname()[:2] # cert = crypto_util.probe_sni( # b'localhost', host=host, port=port, timeout=1) # # Expect normal cert when connecting without ALPN. # self.assertEqual(cert, # self.certs[b'localhost'][1]) def test_challenge_certs(self): host, port = self.server.socket.getsockname()[:2] cert = crypto_util.probe_sni( b'localhost', host=host, port=port, timeout=1, alpn_protocols=[b"acme-tls/1"]) # Expect challenge cert when connecting with ALPN. assert cert == self.challenge_certs[b'localhost'][1] def test_bad_alpn(self): host, port = self.server.socket.getsockname()[:2] with pytest.raises(errors.Error): crypto_util.probe_sni( b'localhost', host=host, port=port, timeout=1, alpn_protocols=[b"bad-alpn"]) class BaseDualNetworkedServersTest(unittest.TestCase): """Test for acme.standalone.BaseDualNetworkedServers.""" class SingleProtocolServer(socketserver.TCPServer): """Server that only serves on a single protocol. FreeBSD has this behavior for AF_INET6.""" def __init__(self, *args, **kwargs): ipv6 = kwargs.pop("ipv6", False) if ipv6: self.address_family = socket.AF_INET6 kwargs["bind_and_activate"] = False else: self.address_family = socket.AF_INET super().__init__(*args, **kwargs) if ipv6: # NB: On Windows, socket.IPPROTO_IPV6 constant may be missing. # We use the corresponding value (41) instead. level = getattr(socket, "IPPROTO_IPV6", 41) self.socket.setsockopt(level, socket.IPV6_V6ONLY, 1) try: self.server_bind() self.server_activate() except: self.server_close() raise @mock.patch("socket.socket.bind") def test_fail_to_bind(self, mock_bind): from errno import EADDRINUSE from acme.standalone import BaseDualNetworkedServers mock_bind.side_effect = OSError(EADDRINUSE, "Fake addr in use error") with pytest.raises(socket.error) as exc_info: BaseDualNetworkedServers( BaseDualNetworkedServersTest.SingleProtocolServer, ('', 0), socketserver.BaseRequestHandler) assert exc_info.value.errno == EADDRINUSE def test_ports_equal(self): from acme.standalone import BaseDualNetworkedServers servers = BaseDualNetworkedServers( BaseDualNetworkedServersTest.SingleProtocolServer, ('', 0), socketserver.BaseRequestHandler) socknames = servers.getsocknames() prev_port = None # assert ports are equal for sockname in socknames: port = sockname[1] if prev_port: assert prev_port == port prev_port = port for server in servers.servers: server.server_close() class HTTP01DualNetworkedServersTest(unittest.TestCase): """Tests for acme.standalone.HTTP01DualNetworkedServers.""" def setUp(self): self.account_key = jose.JWK.load( test_util.load_vector('rsa1024_key.pem')) self.resources: Set = set() from acme.standalone import HTTP01DualNetworkedServers self.servers = HTTP01DualNetworkedServers(('', 0), resources=self.resources) self.port = self.servers.getsocknames()[0][1] self.servers.serve_forever() def tearDown(self): self.servers.shutdown_and_server_close() def test_index(self): response = requests.get( 'http://localhost:{0}'.format(self.port), verify=False) assert response.text == 'ACME client standalone challenge solver' assert response.ok def test_404(self): response = requests.get( 'http://localhost:{0}/foo'.format(self.port), verify=False) assert response.status_code == http_client.NOT_FOUND def _test_http01(self, add): chall = challenges.HTTP01(token=(b'x' * 16)) response, validation = chall.response_and_validation(self.account_key) from acme.standalone import HTTP01RequestHandler resource = HTTP01RequestHandler.HTTP01Resource( chall=chall, response=response, validation=validation) if add: self.resources.add(resource) return resource.response.simple_verify( resource.chall, 'localhost', self.account_key.public_key(), port=self.port) def test_http01_found(self): assert self._test_http01(add=True) def test_http01_not_found(self): assert not self._test_http01(add=False) if __name__ == "__main__": sys.exit(pytest.main(sys.argv[1:] + [__file__])) # pragma: no cover ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1744063413.0 acme-4.0.0/acme/_internal/tests/test_util.py0000664000175000017500000000505514775045665020140 0ustar00willgwillg"""Test utilities. .. warning:: This module is not part of the public API. """ import importlib.resources import os from typing import Callable from cryptography import x509 from cryptography.hazmat.backends import default_backend from cryptography.hazmat.primitives import serialization import josepy as jose from josepy.util import ComparableECKey from OpenSSL import crypto def load_vector(*names): """Load contents of a test vector.""" # luckily, resource_string opens file in binary mode vector_ref = importlib.resources.files(__package__).joinpath('testdata', *names) return vector_ref.read_bytes() def _guess_loader(filename: str, loader_pem: Callable, loader_der: Callable) -> Callable: _, ext = os.path.splitext(filename) if ext.lower() == ".pem": return loader_pem elif ext.lower() == ".der": return loader_der else: # pragma: no cover raise ValueError("Loader could not be recognized based on extension") def _guess_pyopenssl_loader(filename: str, loader_pem: int, loader_der: int) -> int: _, ext = os.path.splitext(filename) if ext.lower() == ".pem": return loader_pem else: # pragma: no cover raise ValueError("Loader could not be recognized based on extension") def load_cert(*names: str) -> x509.Certificate: """Load certificate.""" loader = _guess_loader( names[-1], x509.load_pem_x509_certificate, x509.load_der_x509_certificate ) return loader(load_vector(*names)) def load_csr(*names: str) -> x509.CertificateSigningRequest: """Load certificate request.""" loader = _guess_loader(names[-1], x509.load_pem_x509_csr, x509.load_der_x509_csr) return loader(load_vector(*names)) def load_rsa_private_key(*names): """Load RSA private key.""" loader = _guess_loader(names[-1], serialization.load_pem_private_key, serialization.load_der_private_key) return jose.ComparableRSAKey(loader( load_vector(*names), password=None, backend=default_backend())) def load_ecdsa_private_key(*names): """Load ECDSA private key.""" loader = _guess_loader(names[-1], serialization.load_pem_private_key, serialization.load_der_private_key) return ComparableECKey(loader( load_vector(*names), password=None, backend=default_backend())) def load_pyopenssl_private_key(*names): """Load pyOpenSSL private key.""" loader = _guess_pyopenssl_loader( names[-1], crypto.FILETYPE_PEM, crypto.FILETYPE_ASN1) return crypto.load_privatekey(loader, load_vector(*names)) ././@PaxHeader0000000000000000000000000000003200000000000010210 xustar0026 mtime=1744063414.97155 acme-4.0.0/acme/_internal/tests/testdata/0000775000175000017500000000000014775045667017360 5ustar00willgwillg././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1744063413.0 acme-4.0.0/acme/_internal/tests/testdata/README0000664000175000017500000000153214775045665020237 0ustar00willgwillgIn order for acme.test_util._guess_loader to work properly, make sure to use appropriate extension for vector filenames: .pem for PEM and .der for DER. The following command has been used to generate test keys: for k in 256 512 1024 2048 4096; do openssl genrsa -out rsa${k}_key.pem $k; done and for the CSR: openssl req -key rsa2048_key.pem -new -subj '/CN=example.com' -outform DER > csr.der and for the certificates: openssl req -key rsa2048_key.pem -new -subj '/CN=example.com' -x509 -outform DER > cert.der openssl req -key rsa2048_key.pem -new -subj '/CN=example.com' -x509 > rsa2048_cert.pem openssl req -key rsa1024_key.pem -new -subj '/CN=example.com' -x509 > rsa1024_cert.pem and for the elliptic key curves: openssl genpkey -algorithm EC -out ec_secp384r1.pem -pkeyopt ec_paramgen_curve:P-384 -pkeyopt ec_param_enc:named_curve ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1744063413.0 acme-4.0.0/acme/_internal/tests/testdata/cert-100sans.pem0000664000175000017500000000530014775045665022177 0ustar00willgwillg-----BEGIN CERTIFICATE----- MIIHxDCCB26gAwIBAgIJAOGrG1Un9lHiMA0GCSqGSIb3DQEBCwUAMGQxCzAJBgNV BAgMAkNBMRYwFAYDVQQHDA1TYW4gRnJhbmNpc2NvMScwJQYDVQQLDB5FbGVjdHJv bmljIEZyb250aWVyIEZvdW5kYXRpb24xFDASBgNVBAMMC2V4YW1wbGUuY29tMB4X DTE2MDEwNjE5MDkzN1oXDTE2MDEwNzE5MDkzN1owZDELMAkGA1UECAwCQ0ExFjAU BgNVBAcMDVNhbiBGcmFuY2lzY28xJzAlBgNVBAsMHkVsZWN0cm9uaWMgRnJvbnRp ZXIgRm91bmRhdGlvbjEUMBIGA1UEAwwLZXhhbXBsZS5jb20wXDANBgkqhkiG9w0B AQEFAANLADBIAkEArHVztFHtH92ucFJD/N/HW9AsdRsUuHUBBBDlHwNlRd3fp580 rv2+6QWE30cWgdmJS86ObRz6lUTor4R0T+3C5QIDAQABo4IGATCCBf0wCQYDVR0T BAIwADALBgNVHQ8EBAMCBeAwggXhBgNVHREEggXYMIIF1IIMZXhhbXBsZTEuY29t ggxleGFtcGxlMi5jb22CDGV4YW1wbGUzLmNvbYIMZXhhbXBsZTQuY29tggxleGFt cGxlNS5jb22CDGV4YW1wbGU2LmNvbYIMZXhhbXBsZTcuY29tggxleGFtcGxlOC5j b22CDGV4YW1wbGU5LmNvbYINZXhhbXBsZTEwLmNvbYINZXhhbXBsZTExLmNvbYIN ZXhhbXBsZTEyLmNvbYINZXhhbXBsZTEzLmNvbYINZXhhbXBsZTE0LmNvbYINZXhh bXBsZTE1LmNvbYINZXhhbXBsZTE2LmNvbYINZXhhbXBsZTE3LmNvbYINZXhhbXBs ZTE4LmNvbYINZXhhbXBsZTE5LmNvbYINZXhhbXBsZTIwLmNvbYINZXhhbXBsZTIx LmNvbYINZXhhbXBsZTIyLmNvbYINZXhhbXBsZTIzLmNvbYINZXhhbXBsZTI0LmNv bYINZXhhbXBsZTI1LmNvbYINZXhhbXBsZTI2LmNvbYINZXhhbXBsZTI3LmNvbYIN ZXhhbXBsZTI4LmNvbYINZXhhbXBsZTI5LmNvbYINZXhhbXBsZTMwLmNvbYINZXhh bXBsZTMxLmNvbYINZXhhbXBsZTMyLmNvbYINZXhhbXBsZTMzLmNvbYINZXhhbXBs ZTM0LmNvbYINZXhhbXBsZTM1LmNvbYINZXhhbXBsZTM2LmNvbYINZXhhbXBsZTM3 LmNvbYINZXhhbXBsZTM4LmNvbYINZXhhbXBsZTM5LmNvbYINZXhhbXBsZTQwLmNv bYINZXhhbXBsZTQxLmNvbYINZXhhbXBsZTQyLmNvbYINZXhhbXBsZTQzLmNvbYIN ZXhhbXBsZTQ0LmNvbYINZXhhbXBsZTQ1LmNvbYINZXhhbXBsZTQ2LmNvbYINZXhh bXBsZTQ3LmNvbYINZXhhbXBsZTQ4LmNvbYINZXhhbXBsZTQ5LmNvbYINZXhhbXBs ZTUwLmNvbYINZXhhbXBsZTUxLmNvbYINZXhhbXBsZTUyLmNvbYINZXhhbXBsZTUz LmNvbYINZXhhbXBsZTU0LmNvbYINZXhhbXBsZTU1LmNvbYINZXhhbXBsZTU2LmNv bYINZXhhbXBsZTU3LmNvbYINZXhhbXBsZTU4LmNvbYINZXhhbXBsZTU5LmNvbYIN ZXhhbXBsZTYwLmNvbYINZXhhbXBsZTYxLmNvbYINZXhhbXBsZTYyLmNvbYINZXhh bXBsZTYzLmNvbYINZXhhbXBsZTY0LmNvbYINZXhhbXBsZTY1LmNvbYINZXhhbXBs ZTY2LmNvbYINZXhhbXBsZTY3LmNvbYINZXhhbXBsZTY4LmNvbYINZXhhbXBsZTY5 LmNvbYINZXhhbXBsZTcwLmNvbYINZXhhbXBsZTcxLmNvbYINZXhhbXBsZTcyLmNv bYINZXhhbXBsZTczLmNvbYINZXhhbXBsZTc0LmNvbYINZXhhbXBsZTc1LmNvbYIN ZXhhbXBsZTc2LmNvbYINZXhhbXBsZTc3LmNvbYINZXhhbXBsZTc4LmNvbYINZXhh bXBsZTc5LmNvbYINZXhhbXBsZTgwLmNvbYINZXhhbXBsZTgxLmNvbYINZXhhbXBs ZTgyLmNvbYINZXhhbXBsZTgzLmNvbYINZXhhbXBsZTg0LmNvbYINZXhhbXBsZTg1 LmNvbYINZXhhbXBsZTg2LmNvbYINZXhhbXBsZTg3LmNvbYINZXhhbXBsZTg4LmNv bYINZXhhbXBsZTg5LmNvbYINZXhhbXBsZTkwLmNvbYINZXhhbXBsZTkxLmNvbYIN ZXhhbXBsZTkyLmNvbYINZXhhbXBsZTkzLmNvbYINZXhhbXBsZTk0LmNvbYINZXhh bXBsZTk1LmNvbYINZXhhbXBsZTk2LmNvbYINZXhhbXBsZTk3LmNvbYINZXhhbXBs ZTk4LmNvbYINZXhhbXBsZTk5LmNvbYIOZXhhbXBsZTEwMC5jb20wDQYJKoZIhvcN AQELBQADQQBEunJbKUXcyNKTSfA0pKRyWNiKmkoBqYgfZS6eHNrNH/hjFzHtzyDQ XYHHK6kgEWBvHfRXGmqhFvht+b1tQKkG -----END CERTIFICATE----- ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1744063413.0 acme-4.0.0/acme/_internal/tests/testdata/cert-idnsans.pem0000664000175000017500000000351214775045665022454 0ustar00willgwillg-----BEGIN CERTIFICATE----- MIIFNjCCBOCgAwIBAgIJAP4rNqqOKifCMA0GCSqGSIb3DQEBCwUAMGQxCzAJBgNV BAgMAkNBMRYwFAYDVQQHDA1TYW4gRnJhbmNpc2NvMScwJQYDVQQLDB5FbGVjdHJv bmljIEZyb250aWVyIEZvdW5kYXRpb24xFDASBgNVBAMMC2V4YW1wbGUuY29tMB4X DTE2MDEwNjIwMDg1OFoXDTE2MDEwNzIwMDg1OFowZDELMAkGA1UECAwCQ0ExFjAU BgNVBAcMDVNhbiBGcmFuY2lzY28xJzAlBgNVBAsMHkVsZWN0cm9uaWMgRnJvbnRp ZXIgRm91bmRhdGlvbjEUMBIGA1UEAwwLZXhhbXBsZS5jb20wXDANBgkqhkiG9w0B AQEFAANLADBIAkEArHVztFHtH92ucFJD/N/HW9AsdRsUuHUBBBDlHwNlRd3fp580 rv2+6QWE30cWgdmJS86ObRz6lUTor4R0T+3C5QIDAQABo4IDczCCA28wCQYDVR0T BAIwADALBgNVHQ8EBAMCBeAwggNTBgNVHREEggNKMIIDRoJiz4PPhM+Fz4bPh8+I z4nPis+Lz4zPjc+Oz4/PkM+Rz5LPk8+Uz5XPls+Xz5jPmc+az5vPnM+dz57Pn8+g z6HPos+jz6TPpc+mz6fPqM+pz6rPq8+sz63Prs+vLmludmFsaWSCYs+wz7HPss+z z7TPtc+2z7fPuM+5z7rPu8+8z73Pvs+/2YHZgtmD2YTZhdmG2YfZiNmJ2YrZi9mM 2Y3ZjtmP2ZDZkdmS2ZPZlNmV2ZbZl9mY2ZnZmtmb2ZzZnS5pbnZhbGlkgmLZntmf 2aDZodmi2aPZpNml2abZp9mo2anZqtmr2azZrdmu2a/ZsNmx2bLZs9m02bXZttm3 2bjZudm62bvZvNm92b7Zv9qA2oHagtqD2oTahdqG2ofaiNqJ2oouaW52YWxpZIJi 2ovajNqN2o7aj9qQ2pHaktqT2pTaldqW2pfamNqZ2pram9qc2p3antqf2qDaodqi 2qPapNql2qbap9qo2qnaqtqr2qzardqu2q/asNqx2rLas9q02rXattq3LmludmFs aWSCYtq42rnautq72rzavdq+2r/bgNuB24Lbg9uE24XbhtuH24jbiduK24vbjNuN 247bj9uQ25HbktuT25TblduW25fbmNuZ25rbm9uc253bntuf26Dbodui26PbpC5p bnZhbGlkgnjbpdum26fbqNup26rbq9us263brtuv27Dbsduy27PbtNu127bbt9u4 27nbutu74aCg4aCh4aCi4aCj4aCk4aCl4aCm4aCn4aCo4aCp4aCq4aCr4aCs4aCt 4aCu4aCv4aCw4aCx4aCy4aCz4aC04aC1LmludmFsaWSCgY/hoLbhoLfhoLjhoLnh oLrhoLvhoLzhoL3hoL7hoL/hoYDhoYHhoYLhoYPhoYThoYXhoYbhoYfhoYjhoYnh oYrhoYvhoYzhoY3hoY7hoY/hoZDhoZHhoZLhoZPhoZThoZXhoZbhoZfhoZjhoZnh oZrhoZvhoZzhoZ3hoZ7hoZ/hoaDhoaHhoaIuaW52YWxpZIJE4aGj4aGk4aGl4aGm 4aGn4aGo4aGp4aGq4aGr4aGs4aGt4aGu4aGv4aGw4aGx4aGy4aGz4aG04aG14aG2 LmludmFsaWQwDQYJKoZIhvcNAQELBQADQQAzOQL/54yXxln87/YvEQbBm9ik9zoT TxEkvnZ4kmTRhDsUPtRjMXhY2FH7LOtXKnJQ7POUB7AsJ2Z6uq2w623G -----END CERTIFICATE----- ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1744063413.0 acme-4.0.0/acme/_internal/tests/testdata/cert-ipsans.pem0000664000175000017500000000241114775045665022307 0ustar00willgwillg-----BEGIN CERTIFICATE----- MIIDizCCAnOgAwIBAgIIPNBLQXwhoUkwDQYJKoZIhvcNAQELBQAwKDEmMCQGA1UE AxMdUGViYmxlIEludGVybWVkaWF0ZSBDQSAxNzNiMjYwHhcNMjAwNTI5MTkxODA5 WhcNMjUwNTI5MTkxODA5WjAWMRQwEgYDVQQDEwsxOTIuMC4yLjE0NTCCASIwDQYJ KoZIhvcNAQEBBQADggEPADCCAQoCggEBALyChb+NDA26GF1AfC0nzEdfOTchKw0h q41xEjonvg5UXgZf/aH/ntvugIkYP0MaFifNAjebOVVsemEVEtyWcUKTfBHKZGbZ ukTDwFIjfTccCfo6U/B2H7ZLzJIywl8DcUw9DypadeQBm8PS0VVR2ncy73dvaqym crhAwlASyXU0mhLqRDMMxfg5Bn/FWpcsIcDpLmPn8Q/FvdRc2t5ryBNw/aWOlwqT Oy16nbfLj2T0zG1A3aPuD+eT/JFUe/o3K7R+FAx7wt+RziQO46wLVVF1SueZUrIU zqN04Gl8Kt1WM2SniZ0gq/rORUNcPtT0NAEsEslTQfA+Trq6j2peqyMCAwEAAaOB yjCBxzAOBgNVHQ8BAf8EBAMCBaAwHQYDVR0lBBYwFAYIKwYBBQUHAwEGCCsGAQUF BwMCMAwGA1UdEwEB/wQCMAAwHQYDVR0OBBYEFHj1mwZzP//nMIH2i58NRUl/arHn MB8GA1UdIwQYMBaAFF5DVAKabvIUvKFHGouscA2Qdpe6MDEGCCsGAQUFBwEBBCUw IzAhBggrBgEFBQcwAYYVaHR0cDovLzEyNy4wLjAuMTo0MDAyMBUGA1UdEQQOMAyH BMAAApGHBMsAcQEwDQYJKoZIhvcNAQELBQADggEBAHjSgDg76/UCIMSYddyhj18r LdNKjA7p8ovnErSkebFT4lIZ9f3Sma9moNr0w64M33NamuFyHe/KTdk90mvoW8Uu 26aDekiRIeeMakzbAtDKn67tt2tbedKIYRATcSYVwsV46uZKbM621dZKIjjxOWpo IY6rZYrku8LYhoXJXOqRduV3cTRVuTm5bBa9FfVNtt6N1T5JOtKKDEhuSaF4RSug PDy3hQIiHrVvhPfVrXU3j6owz/8UCS5549inES9ONTFrvM9o0H1R/MsmGNXR5hF5 iJqHKC7n8LZujhVnoFIpHu2Dsiefbfr+yRYJS4I+ezy6Nq/Ok8rc8zp0eoX+uyY= -----END CERTIFICATE----- ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1744063413.0 acme-4.0.0/acme/_internal/tests/testdata/cert-ipv6sans.pem0000664000175000017500000000243614775045665022572 0ustar00willgwillg-----BEGIN CERTIFICATE----- MIIDmzCCAoOgAwIBAgIIFdxeZP+v2rgwDQYJKoZIhvcNAQELBQAwKDEmMCQGA1UE AxMdUGViYmxlIEludGVybWVkaWF0ZSBDQSA0M2M5NTcwHhcNMjAwNTMwMDQwNzMw WhcNMjUwNTMwMDQwNzMwWjAOMQwwCgYDVQQDEwM6OjEwggEiMA0GCSqGSIb3DQEB AQUAA4IBDwAwggEKAoIBAQC7VidVduJvqKtrSH0fw6PjE0cqL4Kfzo7klexWUkHG KVAa0fRVZFZ462jxKOt417V2U4WJQ6WHHO9PJ+3gW62d/MhCw8FRtUQS4nYFjqB6 32+RFU21VRN7cWoQEqSwnEPbh/v/zv/KS5JhQ+swWUo79AOLm1kjnZWCKtcqh1Lc Ug5Tkpot6luoxTKp52MkchvXDpj0q2B/XpLJ8/pw5cqjv7mH12EDOK2HXllA+WwX ZpstcEhaA4FqtaHOW/OHnwTX5MUbINXE5YYHVEDR6moVM31/W/3pe9NDUMTDE7Si lVQnZbXM9NYbzZqlh+WhemDWwnIfGI6rtsfNEiirVEOlAgMBAAGjgeIwgd8wDgYD VR0PAQH/BAQDAgWgMB0GA1UdJQQWMBQGCCsGAQUFBwMBBggrBgEFBQcDAjAMBgNV HRMBAf8EAjAAMB0GA1UdDgQWBBS8DL+MZfDIy6AKky69Tgry2Vxq5DAfBgNVHSME GDAWgBRAsFqVenRRKgB1YPzWKzb9bzZ/ozAxBggrBgEFBQcBAQQlMCMwIQYIKwYB BQUHMAGGFWh0dHA6Ly8xMjcuMC4wLjE6NDAwMjAtBgNVHREEJjAkhxAAAAAAAAAA AAAAAAAAAAABhxCjvjLzIG7HXQlWDO6YWF7FMA0GCSqGSIb3DQEBCwUAA4IBAQBY M9UTZ3uaKMQ+He9kWR3p9jh6hTSD0FNi79ZdfkG0lgSzhhduhN7OhzQH2ihUUfa6 rtKTw74fGbszhizCd9UB8YPKlm3si1Xbg6ZUQlA1RtoQo7RUGEa6ZbR68PKGm9Go hTTFIl/JS8jzxBR8jywZdyqtprUx+nnNUDiNk0hJtFLhw7OJH0AHlAUNqHsfD08m HXRdaV6q14HXU5g31slBat9H4D6tCU/2uqBURwW0wVdnqh4QeRfAeqiatJS9EmSF ctbc7n894Idy2Xce7NFoIy5cht3m6Rd42o/LmBsJopBmQcDPZT70/XzRtc2qE0cS CzBIGQHUJ6BfmBjrCQnp -----END CERTIFICATE----- ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1744063413.0 acme-4.0.0/acme/_internal/tests/testdata/cert-nocn.der0000664000175000017500000000256514775045665021752 0ustar00willgwillg0‚q0‚Y ob¿Å6”à^ôå%0  *†H†÷  0–1 0 UGB10UGreater Manchester10USalford10U COMODO CA Limited1<0:U3COMODO RSA Organization Validation Secure Server CA0 170323000000Z 200622235959Z0k1 0 UUS10U California10U Walnut Creek10U  Lucas Garron10U Multi-Domain SSL0‚"0  *†H†÷ ‚0‚ ‚ÂìøŒî³ØPÕpXÌ“ë\¨hI°"µù•ž±+,v>lÀK`LLê²´À€¶°ùrɆù\A]+qÄK¼é”.P7¦gaŒöABÅFÓ‡'Ÿtë R&!sl„LyUäÑkè=HR­³(Ûªÿnÿ`•Jwk9ñ$Ñ1¶ÝMÀÄüS¹mB­µ|þ®õÒ3Hç"qÇÂzl(ê7Jßêlµr´~Z¢Üi±WDÛ «ÞÃGt\A"ášù“æ­").±ºI 'ž£û‹÷@r¬’ÙŒW„SËæþkT˜@'…Ç»spïiAEU|ùd?=,é|ë“L†ÑÊ…£‚ã0‚ß0U#0€šó+ÚÏ­O¶/»*HH*·BÁ$0UîÁ{ :Giq}7“¼¥?û0Uÿ 0 Uÿ00U%0++0PU I0G0; +²10+0)+https://secure.comodo.com/CPS0g 0ZUS0Q0O M K†Ihttp://crl.comodoca.com/COMODORSAOrganizationValidationSecureServerCA.crl0‹+0}0U+0†Ihttp://crt.comodoca.com/COMODORSAOrganizationValidationSecureServerCA.crt0$+0†http://ocsp.comodoca.com0$U0‚no-common-name.badssl.com0  *†H†÷  ‚iü%FÀžfó»øæ4DÈHœî¢:kDÍ{| p‚5Žï&¡ÒJˆóúœ;Ÿé~«¦òÜâ›Zw‡¶-ô­à­J/¹wÌUÁ¾¤Ø”»¯îŒ?˜3©¯¿Hb¹5Vh3-ü“òfR¸jEZ·mØŒæ,Q”ÌNÀ°?ŠlÏÙ’-…µI0q.Ögdú{³Ò…²±ëÓ£öMB„i³»$ìí¸¦ÂíK–‰n:ž“<‚eaùÐh|ÍH¡ M¬Be!¿@aOM$šâ«Òg!›ÿü²1®,§%¼Ù´®ô‡5ýlðë>²Ëħ)kóŽ!8²LFÒÿ2êW¿iKæôÊj,íF;{f././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1744063413.0 acme-4.0.0/acme/_internal/tests/testdata/cert-san.pem0000664000175000017500000000142214775045665021574 0ustar00willgwillg-----BEGIN CERTIFICATE----- MIICFjCCAcCgAwIBAgICBTkwDQYJKoZIhvcNAQELBQAwdzELMAkGA1UEBhMCVVMx ETAPBgNVBAgMCE1pY2hpZ2FuMRIwEAYDVQQHDAlBbm4gQXJib3IxKzApBgNVBAoM IlVuaXZlcnNpdHkgb2YgTWljaGlnYW4gYW5kIHRoZSBFRkYxFDASBgNVBAMMC2V4 YW1wbGUuY29tMB4XDTE0MTIxMTIyMzQ0NVoXDTE0MTIxODIyMzQ0NVowdzELMAkG A1UEBhMCVVMxETAPBgNVBAgMCE1pY2hpZ2FuMRIwEAYDVQQHDAlBbm4gQXJib3Ix KzApBgNVBAoMIlVuaXZlcnNpdHkgb2YgTWljaGlnYW4gYW5kIHRoZSBFRkYxFDAS BgNVBAMMC2V4YW1wbGUuY29tMFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBAKx1c7RR 7R/drnBSQ/zfx1vQLHUbFLh1AQQQ5R8DZUXd36efNK79vukFhN9HFoHZiUvOjm0c +pVE6K+EdE/twuUCAwEAAaM2MDQwCQYDVR0TBAIwADAnBgNVHREEIDAeggtleGFt cGxlLmNvbYIPd3d3LmV4YW1wbGUuY29tMA0GCSqGSIb3DQEBCwUAA0EASuvNKFTF nTJsvnSXn52f4BMZJJ2id/kW7+r+FJRm+L20gKQ1aqq8d3e/lzRUrv5SMf1TAOe7 RDjyGMKy5ZgM2w== -----END CERTIFICATE----- ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1744063413.0 acme-4.0.0/acme/_internal/tests/testdata/cert.der0000664000175000017500000000140314775045665021005 0ustar00willgwillg0‚ÿ0‚ç  Á‚2WZŒdª0  *†H†÷  010U example.com0 151028072441Z 151127072441Z010U example.com0‚"0  *†H†÷ ‚0‚ ‚ð|0wˆ˜â3Ïçð$ÂG‚žav€±ŽÞÿ1.k>=a÷e¼\u[DŸnXß‹"¢:áÄjP]t±ÊeMÞpŸš›© ¹¹ÛŽ*ô厘8(¸iú•Ãf 3-/Üe\všóuãi ÔD™KªîlKƒË· Æn€šÇþaÁl3gð×3$áa°LOØGLç) 3S¦ñª“ª£Ì€û›ëÏëá,¦øËžôjå s‡øÂ|àH"0ƒycî_ÈMä=À.Û¢ºh§5J‹Ihº¿^+üWɯž¸FîÐø½Ws½;?Gsåƒ9_̆eÕÆžþT4)³k§£P0N0Uô6 òÉ )óõ–Ö+e£´¼X70U#0€ô6 òÉ )óõ–Ö+e£´¼X70 U0ÿ0  *†H†÷  ‚íTí2Ùðý£Oú]œíÄJ©7Ý öêÔ£­N^ÀÍÆ ·ºùxê°1µœ§ŽžÓ[òIK…,=^µ2k뮫mHGï[2”Ìü-ŽmÅlhf¸ll@Õ'Dp¥ˆH4¤†ªC¸'µñ{[øC c6¸0u;™d“è FõµAá›zk&eP Ǫa3ú¿šEu…ñP‚jW¼3uô„Îö ³› ó,‰ÖÚS—Ò~äC6Q8²YèOºfs˜ÓÙ U«pªM!È‘)+–m,(hEâ °{b ûÐãë³KêR¼V¢hæÙÅ—‡ô ;ÛÚlv1æ’././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1744063413.0 acme-4.0.0/acme/_internal/tests/testdata/cert.pem0000664000175000017500000000130514775045665021015 0ustar00willgwillg-----BEGIN CERTIFICATE----- MIIB3jCCAYigAwIBAgICBTkwDQYJKoZIhvcNAQELBQAwdzELMAkGA1UEBhMCVVMx ETAPBgNVBAgMCE1pY2hpZ2FuMRIwEAYDVQQHDAlBbm4gQXJib3IxKzApBgNVBAoM IlVuaXZlcnNpdHkgb2YgTWljaGlnYW4gYW5kIHRoZSBFRkYxFDASBgNVBAMMC2V4 YW1wbGUuY29tMB4XDTE0MTIxMTIyMzQ0NVoXDTE0MTIxODIyMzQ0NVowdzELMAkG A1UEBhMCVVMxETAPBgNVBAgMCE1pY2hpZ2FuMRIwEAYDVQQHDAlBbm4gQXJib3Ix KzApBgNVBAoMIlVuaXZlcnNpdHkgb2YgTWljaGlnYW4gYW5kIHRoZSBFRkYxFDAS BgNVBAMMC2V4YW1wbGUuY29tMFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBAKx1c7RR 7R/drnBSQ/zfx1vQLHUbFLh1AQQQ5R8DZUXd36efNK79vukFhN9HFoHZiUvOjm0c +pVE6K+EdE/twuUCAwEAATANBgkqhkiG9w0BAQsFAANBAC24z0IdwIVKSlntksll vr6zJepBH5fMndfk3XJp10jT6VE+14KNtjh02a56GoraAvJAT5/H67E8GvJ/ocNn B/o= -----END CERTIFICATE----- ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1744063413.0 acme-4.0.0/acme/_internal/tests/testdata/critical-san.pem0000664000175000017500000000322314775045665022432 0ustar00willgwillg-----BEGIN CERTIFICATE----- MIIErTCCA5WgAwIBAgIKETb7VQAAAAAdGTANBgkqhkiG9w0BAQsFADCBkTELMAkG A1UEBhMCVVMxDTALBgNVBAgTBFV0YWgxFzAVBgNVBAcTDlNhbHQgTGFrZSBDaXR5 MRUwEwYDVQQKEwxWZW5hZmksIEluYy4xHzAdBgNVBAsTFkRlbW9uc3RyYXRpb24g U2VydmljZXMxIjAgBgNVBAMTGVZlbmFmaSBFeGFtcGxlIElzc3VpbmcgQ0EwHhcN MTcwNzEwMjMxNjA1WhcNMTcwODA5MjMxNjA1WjAAMIIBIjANBgkqhkiG9w0BAQEF AAOCAQ8AMIIBCgKCAQEA7CU5qRIzCs9hCRiSUvLZ8r81l4zIYbx1V1vZz6x1cS4M 0keNfFJ1wB+zuvx80KaMYkWPYlg4Rsm9Ok3ZapakXDlaWtrfg78lxtHuPw1o7AYV EXDwwPkNugLMJfYw5hWYSr8PCLcOJoY00YQ0fJ44L+kVsUyGjN4UTRRZmOh/yNVU 0W12dTCz4X7BAW01OuY6SxxwewnW3sBEep+APfr2jd/oIx7fgZmVB8aRCDPj4AFl XINWIwxmptOwnKPbwLN/vhCvJRUkO6rA8lpYwQkedFf6fHhqi2Sq/NCEOg4RvMCF fKbMpncOXxz+f4/i43SVLrPz/UyhjNbKGJZ+zFrQowIDAQABo4IBlTCCAZEwPgYD VR0RAQH/BDQwMoIbY2hpY2Fnby1jdWJzLnZlbmFmaS5leGFtcGxlghNjdWJzLnZl bmFmaS5leGFtcGxlMB0GA1UdDgQWBBTgKZXVSFNyPHHtO/phtIALPcCF5DAfBgNV HSMEGDAWgBT/JJ6Wei/pzf+9DRHuv6Wgdk2HsjBSBgNVHR8ESzBJMEegRaBDhkFo dHRwOi8vcGtpLnZlbmFmaS5leGFtcGxlL2NybC9WZW5hZmklMjBFeGFtcGxlJTIw SXNzdWluZyUyMENBLmNybDA6BggrBgEFBQcBAQQuMCwwKgYIKwYBBQUHMAGGHmh0 dHA6Ly9wa2kudmVuYWZpLmV4YW1wbGUvb2NzcDAOBgNVHQ8BAf8EBAMCBaAwPQYJ KwYBBAGCNxUHBDAwLgYmKwYBBAGCNxUIhIDLGYTvsSSEnZ8ehvD5UofP4hMEgobv DIGy4mcCAWQCAQIwEwYDVR0lBAwwCgYIKwYBBQUHAwEwGwYJKwYBBAGCNxUKBA4w DDAKBggrBgEFBQcDATANBgkqhkiG9w0BAQsFAAOCAQEA3YW4t1AzxEn384OqdU6L ny8XkMhWpRM0W0Z9ZC3gRZKbVUu49nG/KB5hbVn/de33zdX9HOZJKc0vXzkGZQUs OUCCsKX4VKzV5naGXOuGRbvV4CJh5P0kPlDzyb5t312S49nJdcdBf0Y/uL5Qzhst bXy8qNfFNG3SIKKRAUpqE9OVIl+F+JBwexa+v/4dFtUOqMipfXxB3TaxnDqvU1dS yO34ZTvIMGXJIZ5nn/d/LNc3N3vBg2SHkMpladqw0Hr7mL0bFOe0b+lJgkDP06Be n08fikhz1j2AW4/ZHa9w4DUz7J21+RtHMhh+Vd1On0EAeZ563svDe7Z+yrg6zOVv KA== -----END CERTIFICATE-----././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1744063413.0 acme-4.0.0/acme/_internal/tests/testdata/csr-100sans.pem0000664000175000017500000000502114775045665022031 0ustar00willgwillg-----BEGIN CERTIFICATE REQUEST----- MIIHNTCCBt8CAQAwZDELMAkGA1UECAwCQ0ExFjAUBgNVBAcMDVNhbiBGcmFuY2lz Y28xJzAlBgNVBAsMHkVsZWN0cm9uaWMgRnJvbnRpZXIgRm91bmRhdGlvbjEUMBIG A1UEAwwLZXhhbXBsZS5jb20wXDANBgkqhkiG9w0BAQEFAANLADBIAkEArHVztFHt H92ucFJD/N/HW9AsdRsUuHUBBBDlHwNlRd3fp580rv2+6QWE30cWgdmJS86ObRz6 lUTor4R0T+3C5QIDAQABoIIGFDCCBhAGCSqGSIb3DQEJDjGCBgEwggX9MAkGA1Ud EwQCMAAwCwYDVR0PBAQDAgXgMIIF4QYDVR0RBIIF2DCCBdSCDGV4YW1wbGUxLmNv bYIMZXhhbXBsZTIuY29tggxleGFtcGxlMy5jb22CDGV4YW1wbGU0LmNvbYIMZXhh bXBsZTUuY29tggxleGFtcGxlNi5jb22CDGV4YW1wbGU3LmNvbYIMZXhhbXBsZTgu Y29tggxleGFtcGxlOS5jb22CDWV4YW1wbGUxMC5jb22CDWV4YW1wbGUxMS5jb22C DWV4YW1wbGUxMi5jb22CDWV4YW1wbGUxMy5jb22CDWV4YW1wbGUxNC5jb22CDWV4 YW1wbGUxNS5jb22CDWV4YW1wbGUxNi5jb22CDWV4YW1wbGUxNy5jb22CDWV4YW1w bGUxOC5jb22CDWV4YW1wbGUxOS5jb22CDWV4YW1wbGUyMC5jb22CDWV4YW1wbGUy MS5jb22CDWV4YW1wbGUyMi5jb22CDWV4YW1wbGUyMy5jb22CDWV4YW1wbGUyNC5j b22CDWV4YW1wbGUyNS5jb22CDWV4YW1wbGUyNi5jb22CDWV4YW1wbGUyNy5jb22C DWV4YW1wbGUyOC5jb22CDWV4YW1wbGUyOS5jb22CDWV4YW1wbGUzMC5jb22CDWV4 YW1wbGUzMS5jb22CDWV4YW1wbGUzMi5jb22CDWV4YW1wbGUzMy5jb22CDWV4YW1w bGUzNC5jb22CDWV4YW1wbGUzNS5jb22CDWV4YW1wbGUzNi5jb22CDWV4YW1wbGUz Ny5jb22CDWV4YW1wbGUzOC5jb22CDWV4YW1wbGUzOS5jb22CDWV4YW1wbGU0MC5j b22CDWV4YW1wbGU0MS5jb22CDWV4YW1wbGU0Mi5jb22CDWV4YW1wbGU0My5jb22C DWV4YW1wbGU0NC5jb22CDWV4YW1wbGU0NS5jb22CDWV4YW1wbGU0Ni5jb22CDWV4 YW1wbGU0Ny5jb22CDWV4YW1wbGU0OC5jb22CDWV4YW1wbGU0OS5jb22CDWV4YW1w bGU1MC5jb22CDWV4YW1wbGU1MS5jb22CDWV4YW1wbGU1Mi5jb22CDWV4YW1wbGU1 My5jb22CDWV4YW1wbGU1NC5jb22CDWV4YW1wbGU1NS5jb22CDWV4YW1wbGU1Ni5j b22CDWV4YW1wbGU1Ny5jb22CDWV4YW1wbGU1OC5jb22CDWV4YW1wbGU1OS5jb22C DWV4YW1wbGU2MC5jb22CDWV4YW1wbGU2MS5jb22CDWV4YW1wbGU2Mi5jb22CDWV4 YW1wbGU2My5jb22CDWV4YW1wbGU2NC5jb22CDWV4YW1wbGU2NS5jb22CDWV4YW1w bGU2Ni5jb22CDWV4YW1wbGU2Ny5jb22CDWV4YW1wbGU2OC5jb22CDWV4YW1wbGU2 OS5jb22CDWV4YW1wbGU3MC5jb22CDWV4YW1wbGU3MS5jb22CDWV4YW1wbGU3Mi5j b22CDWV4YW1wbGU3My5jb22CDWV4YW1wbGU3NC5jb22CDWV4YW1wbGU3NS5jb22C DWV4YW1wbGU3Ni5jb22CDWV4YW1wbGU3Ny5jb22CDWV4YW1wbGU3OC5jb22CDWV4 YW1wbGU3OS5jb22CDWV4YW1wbGU4MC5jb22CDWV4YW1wbGU4MS5jb22CDWV4YW1w bGU4Mi5jb22CDWV4YW1wbGU4My5jb22CDWV4YW1wbGU4NC5jb22CDWV4YW1wbGU4 NS5jb22CDWV4YW1wbGU4Ni5jb22CDWV4YW1wbGU4Ny5jb22CDWV4YW1wbGU4OC5j b22CDWV4YW1wbGU4OS5jb22CDWV4YW1wbGU5MC5jb22CDWV4YW1wbGU5MS5jb22C DWV4YW1wbGU5Mi5jb22CDWV4YW1wbGU5My5jb22CDWV4YW1wbGU5NC5jb22CDWV4 YW1wbGU5NS5jb22CDWV4YW1wbGU5Ni5jb22CDWV4YW1wbGU5Ny5jb22CDWV4YW1w bGU5OC5jb22CDWV4YW1wbGU5OS5jb22CDmV4YW1wbGUxMDAuY29tMA0GCSqGSIb3 DQEBCwUAA0EAW05UMFavHn2rkzMyUfzsOvWzVNlm43eO2yHu5h5TzDb23gkDnNEo duUAbQ+CLJHYd+MvRCmPQ+3ZnaPy7l/0Hg== -----END CERTIFICATE REQUEST----- ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1744063413.0 acme-4.0.0/acme/_internal/tests/testdata/csr-6sans.pem0000664000175000017500000000124414775045665021701 0ustar00willgwillg-----BEGIN CERTIFICATE REQUEST----- MIIBuzCCAWUCAQAweTELMAkGA1UEBhMCVVMxETAPBgNVBAgTCE1pY2hpZ2FuMRIw EAYDVQQHEwlBbm4gQXJib3IxDDAKBgNVBAoTA0VGRjEfMB0GA1UECxMWVW5pdmVy c2l0eSBvZiBNaWNoaWdhbjEUMBIGA1UEAxMLZXhhbXBsZS5jb20wXDANBgkqhkiG 9w0BAQEFAANLADBIAkEA9LYRcVE3Nr+qleecEcX8JwVDnjeG1X7ucsCasuuZM0e0 9cmYuUzxIkMjO/9x4AVcvXXRXPEV+LzWWkfkTlzRMwIDAQABoIGGMIGDBgkqhkiG 9w0BCQ4xdjB0MHIGA1UdEQRrMGmCC2V4YW1wbGUuY29tggtleGFtcGxlLm9yZ4IL ZXhhbXBsZS5uZXSCDGV4YW1wbGUuaW5mb4IVc3ViZG9tYWluLmV4YW1wbGUuY29t ghtvdGhlci5zdWJkb21haW4uZXhhbXBsZS5jb20wDQYJKoZIhvcNAQELBQADQQBd k4BE5qvEvkYoZM/2++Xd9RrQ6wsdj0QiJQCozfsI4lQx6ZJnbtNc7HpDrX4W6XIv IvzVBz/nD11drfz/RNuX -----END CERTIFICATE REQUEST----- ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1744063413.0 acme-4.0.0/acme/_internal/tests/testdata/csr-idnsans.pem0000664000175000017500000000323314775045665022306 0ustar00willgwillg-----BEGIN CERTIFICATE REQUEST----- MIIEpzCCBFECAQAwZDELMAkGA1UECAwCQ0ExFjAUBgNVBAcMDVNhbiBGcmFuY2lz Y28xJzAlBgNVBAsMHkVsZWN0cm9uaWMgRnJvbnRpZXIgRm91bmRhdGlvbjEUMBIG A1UEAwwLZXhhbXBsZS5jb20wXDANBgkqhkiG9w0BAQEFAANLADBIAkEArHVztFHt H92ucFJD/N/HW9AsdRsUuHUBBBDlHwNlRd3fp580rv2+6QWE30cWgdmJS86ObRz6 lUTor4R0T+3C5QIDAQABoIIDhjCCA4IGCSqGSIb3DQEJDjGCA3MwggNvMAkGA1Ud EwQCMAAwCwYDVR0PBAQDAgXgMIIDUwYDVR0RBIIDSjCCA0aCYs+Dz4TPhc+Gz4fP iM+Jz4rPi8+Mz43Pjs+Pz5DPkc+Sz5PPlM+Vz5bPl8+Yz5nPms+bz5zPnc+ez5/P oM+hz6LPo8+kz6XPps+nz6jPqc+qz6vPrM+tz67Pry5pbnZhbGlkgmLPsM+xz7LP s8+0z7XPts+3z7jPuc+6z7vPvM+9z77Pv9mB2YLZg9mE2YXZhtmH2YjZidmK2YvZ jNmN2Y7Zj9mQ2ZHZktmT2ZTZldmW2ZfZmNmZ2ZrZm9mc2Z0uaW52YWxpZIJi2Z7Z n9mg2aHZotmj2aTZpdmm2afZqNmp2arZq9ms2a3Zrtmv2bDZsdmy2bPZtNm12bbZ t9m42bnZutm72bzZvdm+2b/agNqB2oLag9qE2oXahtqH2ojaidqKLmludmFsaWSC YtqL2ozajdqO2o/akNqR2pLak9qU2pXaltqX2pjamdqa2pvanNqd2p7an9qg2qHa otqj2qTapdqm2qfaqNqp2qraq9qs2q3artqv2rDasdqy2rPatNq12rbaty5pbnZh bGlkgmLauNq52rrau9q82r3avtq/24DbgduC24PbhNuF24bbh9uI24nbituL24zb jduO24/bkNuR25Lbk9uU25XbltuX25jbmdua25vbnNud257bn9ug26Hbotuj26Qu aW52YWxpZIJ426Xbptun26jbqduq26vbrNut267br9uw27Hbstuz27Tbtdu227fb uNu527rbu+GgoOGgoeGgouGgo+GgpOGgpeGgpuGgp+GgqOGgqeGgquGgq+GgrOGg reGgruGgr+GgsOGgseGgsuGgs+GgtOGgtS5pbnZhbGlkgoGP4aC24aC34aC44aC5 4aC64aC74aC84aC94aC+4aC/4aGA4aGB4aGC4aGD4aGE4aGF4aGG4aGH4aGI4aGJ 4aGK4aGL4aGM4aGN4aGO4aGP4aGQ4aGR4aGS4aGT4aGU4aGV4aGW4aGX4aGY4aGZ 4aGa4aGb4aGc4aGd4aGe4aGf4aGg4aGh4aGiLmludmFsaWSCROGho+GhpOGhpeGh puGhp+GhqOGhqeGhquGhq+GhrOGhreGhruGhr+GhsOGhseGhsuGhs+GhtOGhteGh ti5pbnZhbGlkMA0GCSqGSIb3DQEBCwUAA0EAeNkY0M0+kMnjRo6dEUoGE4dX9fEr dfGrpPUBcwG0P5QBdZJWvZxTfRl14yuPYHbGHULXeGqRdkU6HK5pOlzpng== -----END CERTIFICATE REQUEST----- ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1744063413.0 acme-4.0.0/acme/_internal/tests/testdata/csr-ipsans.pem0000664000175000017500000000163014775045665022143 0ustar00willgwillg-----BEGIN CERTIFICATE REQUEST----- MIICbTCCAVUCAQIwADCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAKT/ CE7Y5EYBvI4p7Frt763upIKHDHO/R5/TWMjG8Jm9qTMui8sbMgyh2Yh+lR/j/5Xd tQrhgC6wx10MrW2+3JtYS88HP1p6si8zU1dbK34n3NyyklR2RivW0R7dXgnYNy7t 5YcDYLCrbRMIPINV/uHrmzIHWYUDNcZVdAfIM2AHfKYuV6Mepcn///5GR+l4GcAh Nkf9CW8OdAIuKdbyLCxVr0mUW/vJz1b12uxPsgUdax9sjXgZdT4pfMXADsFd1NeF atpsXU073inqtHru+2F9ijHTQ75TC+u/rr6eYl3BnBntac0gp/ADtDBii7/Q1JOO Bhq7xJNqqxIEdiyM7zcCAwEAAaAoMCYGCSqGSIb3DQEJDjEZMBcwFQYDVR0RBA4w DIcEwAACkYcEywBxATANBgkqhkiG9w0BAQsFAAOCAQEADG5g3zdbSCaXpZhWHkzE Mek3f442TUE1pB+ITRpthmM4N3zZWETYmbLCIAO624uMrRnbCCMvAoLs/L/9ETg/ XMMFtonQC8u9i9tV8B1ceBh8lpIfa+8b9TMWH3bqnrbWQ+YIl+Yd0gXiCZWJ9vK4 eM1Gddu/2bR6s/k4h/XAWRgEexqk57EHr1z0N+T9OoX939n3mVcNI+u9kfd5VJ0z VyA3R8WR6T6KlEl5P5pcWe5Kuyhi7xMmLVImXqBtvKq4O1AMfM+gQr/yn9aE8IRq khP7JrMBLUIub1c/qu2TfvnynNPSM/ZcOX+6PHdHmRkR3nI0Ndpv7Ntv31FTplAm Dw== -----END CERTIFICATE REQUEST----- ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1744063413.0 acme-4.0.0/acme/_internal/tests/testdata/csr-ipv6sans.pem0000664000175000017500000000167014775045665022423 0ustar00willgwillg-----BEGIN CERTIFICATE REQUEST----- MIIChTCCAW0CAQIwADCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAOIc UAppcqJfTkSqqOFqGt1v7lIJZPOcF4bcKI3d5cHAGbOuVxbC7uMaDuObwYLzoiED qnvs1NaEq2phO6KsgGESB7IE2LUjJivO7OnSZjNRpL5si/9egvBiNCn/50lULaWG gLEuyMfk3awZy2mVAymy7Grhbx069A4TH8TqsHuq2RpKyuDL27e/jUt6yYecb3pu hWMiWy3segif4tI46pkOW0/I6DpxyYD2OqOvzxm/voS9RMqE2+7YJA327H7bEi3N lJZEZ1zy7clZ9ga5fBQaetzbg2RyxTrZ7F919NQXSFoXgxb10Eg64wIpz0L3ooCm GEHehsZZexa3J5ccIvMCAwEAAaBAMD4GCSqGSIb3DQEJDjExMC8wLQYDVR0RBCYw JIcQAAAAAAAAAAAAAAAAAAAAAYcQo74y8yBux10JVgzumFhexTANBgkqhkiG9w0B AQsFAAOCAQEALvwVn0A/JPTCiNzcozHFnp5M23C9PXCplWc5u4k34d4XXzpSeFDz fL4gy7NpYIueme2K2ppw2j3PNQUdR6vQ5a75sriegWYrosL+7Q6Joh51ZyEUZQoD mNl4M4S4oX85EaChR6NFGBywTfjFarYi32XBTbFE7rK8N8KM+DQkNdwL1MXqaHWz F1obQKpNXlLedbCBOteV5Eg4zG3565zu/Gw/NhwzzV3mQmgxUcd1sMJxAfHQz4Vl ImLL+xMcR03nDsH2bgtDbK2tJm7WszSxA9tC+Xp2lRewxrnQloRWPYDz177WGQ5Q SoGDzTTtA6uWZxG8h7CkNLOGvA8LtU2rNA== -----END CERTIFICATE REQUEST----- ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1744063413.0 acme-4.0.0/acme/_internal/tests/testdata/csr-mixed.pem0000664000175000017500000000164414775045665021761 0ustar00willgwillg-----BEGIN CERTIFICATE REQUEST----- MIICdjCCAV4CAQAwADCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANoV T1pdvRUUBOqvm7M2ebLEHV7higUH7qAGUZEkfP6W4YriYVY+IHrH1svNPSa+oPTK 7weDNmT11ehWnGyECIM9z2r2Hi9yVV0ycxh4hWQ4Nt8BAKZwCwaXpyWm7Gj6m2Ez pSN5Dd67g5YAQBrUUh1+RRbFi9c0Ls/6ZOExMvfg8kqt4c2sXCgH1IFnxvvOjBYo p7xh0x3L1Akyax0tw8qgQp/z5mkupmVDNJYPFmbzFPMNyDR61ed6QUTDg7P4UAuF kejLLzFvz5YaO7vC+huaTuPhInAhpzqpr4yU97KIjos2/83Itu/Cv8U1RAeEeRTk h0WjUfltoem/5f8bIdsCAwEAAaAxMC8GCSqGSIb3DQEJDjEiMCAwHgYDVR0RBBcw FYINYS5leGVtcGxlLmNvbYcEwAACbzANBgkqhkiG9w0BAQsFAAOCAQEAQ7n/hYen 5INHlcslHPYCQ/BAbX6Ou+Y8hUu8puWNVpE2OM95L2C87jbWwTmCRnkFBwtyoNqo j3DXVW2RYv8y/exq7V6Y5LtpHTgwfugINJ3XlcVzA4Vnf1xqOxv3kwejkq74RuXn xd5N28srgiFqb0e4tOAWVI8Tw27bgBqjoXl0QDFPZpctqUia5bcDJ9WzNSM7VaO1 CBNGHBRz+zL8sqoqJA4HV58tjcgzl+1RtGM+iUHxXpnH+aCNKWIUINrAzIm4Sm00 93RJjhb1kdNR0BC7ikWVbAWaVviHdvATK/RfpmhWDqfEaNgBpvT91GnkhpzctSFD ro0yCUUXXrIr0w== -----END CERTIFICATE REQUEST----- ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1744063413.0 acme-4.0.0/acme/_internal/tests/testdata/csr-nosans.pem0000664000175000017500000000070414775045665022150 0ustar00willgwillg-----BEGIN CERTIFICATE REQUEST----- MIIBFTCBwAIBADBbMQswCQYDVQQGEwJBVTETMBEGA1UECAwKU29tZS1TdGF0ZTEh MB8GA1UECgwYSW50ZXJuZXQgV2lkZ2l0cyBQdHkgTHRkMRQwEgYDVQQDDAtleGFt cGxlLm9yZzBcMA0GCSqGSIb3DQEBAQUAA0sAMEgCQQD0thFxUTc2v6qV55wRxfwn BUOeN4bVfu5ywJqy65kzR7T1yZi5TPEiQyM7/3HgBVy9ddFc8RX4vNZaR+ROXNEz AgMBAAGgADANBgkqhkiG9w0BAQsFAANBAMikGL8Ch7hQCStXH7chhDp6+pt2+VSo wgsrPQ2Bw4veDMlSemUrH+4e0TwbbntHfvXTDHWs9P3BiIDJLxFrjuA= -----END CERTIFICATE REQUEST----- ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1744063413.0 acme-4.0.0/acme/_internal/tests/testdata/csr-san.pem0000664000175000017500000000107614775045665021433 0ustar00willgwillg-----BEGIN CERTIFICATE REQUEST----- MIIBbjCCARgCAQAweTELMAkGA1UEBhMCVVMxETAPBgNVBAgMCE1pY2hpZ2FuMRIw EAYDVQQHDAlBbm4gQXJib3IxDDAKBgNVBAoMA0VGRjEfMB0GA1UECwwWVW5pdmVy c2l0eSBvZiBNaWNoaWdhbjEUMBIGA1UEAwwLZXhhbXBsZS5jb20wXDANBgkqhkiG 9w0BAQEFAANLADBIAkEArHVztFHtH92ucFJD/N/HW9AsdRsUuHUBBBDlHwNlRd3f p580rv2+6QWE30cWgdmJS86ObRz6lUTor4R0T+3C5QIDAQABoDowOAYJKoZIhvcN AQkOMSswKTAnBgNVHREEIDAeggtleGFtcGxlLmNvbYIPd3d3LmV4YW1wbGUuY29t MA0GCSqGSIb3DQEBCwUAA0EAZGBM8J1rRs7onFgtc76mOeoT1c3v0ZsEmxQfb2Wy tmReY6X1N4cs38D9VSow+VMRu2LWkKvzS7RUFSaTaeQz1A== -----END CERTIFICATE REQUEST----- ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1744063413.0 acme-4.0.0/acme/_internal/tests/testdata/csr.der0000664000175000017500000000113714775045665020643 0ustar00willgwillg0‚[0‚C010U example.com0‚"0  *†H†÷ ‚0‚ ‚ð|0wˆ˜â3Ïçð$ÂG‚žav€±ŽÞÿ1.k>=a÷e¼\u[DŸnXß‹"¢:áÄjP]t±ÊeMÞpŸš›© ¹¹ÛŽ*ô厘8(¸iú•Ãf 3-/Üe\všóuãi ÔD™KªîlKƒË· Æn€šÇþaÁl3gð×3$áa°LOØGLç) 3S¦ñª“ª£Ì€û›ëÏëá,¦øËžôjå s‡øÂ|àH"0ƒycî_ÈMä=À.Û¢ºh§5J‹Ihº¿^+üWɯž¸FîÐø½Ws½;?Gsåƒ9_̆eÕÆžþT4)³k§ 0  *†H†÷  ‚rö0Jê0*¢ŸŒ»;s kµÀöÏ[-ÑI%­æI!¤6gu%‘i¯ãÌ8OÔû¥ð}‹D6¤±>é:€}¬_QMKYBÃÄ4a8ôÄÔòš‡×6Ì1þpÀ @T EXá•ÁëáUž¯¥ÅBï• PrÅ>h†6•«(•.¢Îœhw Ù–€qa›ÎQöÉÍ_²PÎâ—kÏNš=ÖEК¢S?ôýE ëÎ %îO±™d±Ñ»ÓK=ª.ðgh¥›ì²¶½î³¢Be+õ_)uUusæZQÃVûù8ŸÅês0Qˆr½;ù`ÅzŒä=FI´././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1744063413.0 acme-4.0.0/acme/_internal/tests/testdata/csr.pem0000664000175000017500000000104614775045665020651 0ustar00willgwillg-----BEGIN CERTIFICATE REQUEST----- MIIBXTCCAQcCAQAweTELMAkGA1UEBhMCVVMxETAPBgNVBAgMCE1pY2hpZ2FuMRIw EAYDVQQHDAlBbm4gQXJib3IxDDAKBgNVBAoMA0VGRjEfMB0GA1UECwwWVW5pdmVy c2l0eSBvZiBNaWNoaWdhbjEUMBIGA1UEAwwLZXhhbXBsZS5jb20wXDANBgkqhkiG 9w0BAQEFAANLADBIAkEArHVztFHtH92ucFJD/N/HW9AsdRsUuHUBBBDlHwNlRd3f p580rv2+6QWE30cWgdmJS86ObRz6lUTor4R0T+3C5QIDAQABoCkwJwYJKoZIhvcN AQkOMRowGDAWBgNVHREEDzANggtleGFtcGxlLmNvbTANBgkqhkiG9w0BAQsFAANB AHJH/O6BtC9aGzEVCMGOZ7z9iIRHWSzr9x/bOzn7hLwsbXPAgO1QxEwL+X+4g20G n9XBE1N9W6HCIEut2d8wACg= -----END CERTIFICATE REQUEST----- ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1744063413.0 acme-4.0.0/acme/_internal/tests/testdata/dsa512_key.pem0000664000175000017500000000125414775045665021732 0ustar00willgwillg-----BEGIN DSA PARAMETERS----- MIGdAkEAwebEoGBfokKQeALHHnAZMQwYU35ILEBdV8oUmzv7qpSVUoHihyqfn6GC OixAKSP8EJYcTilIqPbFbfFyOPlbLwIVANoFHEDiQgknAvKrG78pHzAJdQSPAkEA qfka5Bnl+CeEMpzVZGrOVqZE/LFdZK9eT6YtWjzqtIkf3hwXUVxJsTnBG4xmrfvl 41pgNJpgu99YOYqPpS0g7A== -----END DSA PARAMETERS----- -----BEGIN DSA PRIVATE KEY----- MIH5AgEAAkEAwebEoGBfokKQeALHHnAZMQwYU35ILEBdV8oUmzv7qpSVUoHihyqf n6GCOixAKSP8EJYcTilIqPbFbfFyOPlbLwIVANoFHEDiQgknAvKrG78pHzAJdQSP AkEAqfka5Bnl+CeEMpzVZGrOVqZE/LFdZK9eT6YtWjzqtIkf3hwXUVxJsTnBG4xm rfvl41pgNJpgu99YOYqPpS0g7AJATQ2LUzjGQSM6UljcPY5I2OD9THkUR9kH2tth zZd70UoI9btrVaTizgqYShuok94glSQNK0H92JgUk3scJPaAkAIVAMDn61h6vrCE mNv063So6E+eYaIN -----END DSA PRIVATE KEY----- ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1744063413.0 acme-4.0.0/acme/_internal/tests/testdata/ec_secp384r1_key.pem0000664000175000017500000000046214775045665023036 0ustar00willgwillg-----BEGIN PRIVATE KEY----- MIG2AgEAMBAGByqGSM49AgEGBSuBBAAiBIGeMIGbAgEBBDArTn0pbFk3xHfKeXte xJgS4JVdJQ8mqvezhaNpULZPnwb+mcKLlrj6f5SRM52yREGhZANiAAQcrMoPMVqV rHnDGGz5HUKLNmXfChlNgsrwsruawXF+M283CA6eckAjTXNyiC/ounWmvtoKsZG0 2UQOfQUNSCANId/r986yRGc03W6RJSkcRp86qBYjNsLgbZpber/3+M4= -----END PRIVATE KEY----- ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1744063413.0 acme-4.0.0/acme/_internal/tests/testdata/rsa1024_cert.pem0000664000175000017500000000135514775045665022176 0ustar00willgwillg-----BEGIN CERTIFICATE----- MIIB/TCCAWagAwIBAgIJAOyRIBs3QT8QMA0GCSqGSIb3DQEBCwUAMBYxFDASBgNV BAMMC2V4YW1wbGUuY29tMB4XDTE4MDQyMzEwMzE0NFoXDTE4MDUyMzEwMzE0NFow FjEUMBIGA1UEAwwLZXhhbXBsZS5jb20wgZ8wDQYJKoZIhvcNAQEBBQADgY0AMIGJ AoGBAJqJ87R8aVwByONxgQA9hwgvQd/QqI1r1UInXhEF2VnEtZGtUWLi100IpIqr Mq4qusDwNZ3g8cUPtSkvJGs89djoajMDIJP7lQUEKUYnYrI0q755Tr/DgLWSk7iW l5ezym0VzWUD0/xXUz8yRbNMTjTac80rS5SZk2ja2wWkYlRJAgMBAAGjUzBRMB0G A1UdDgQWBBSsaX0IVZ4XXwdeffVAbG7gnxSYjTAfBgNVHSMEGDAWgBSsaX0IVZ4X XwdeffVAbG7gnxSYjTAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA4GB ADe7SVmvGH2nkwVfONk8TauRUDkePN1CJZKFb2zW1uO9ANJ2v5Arm/OQp0BG/xnI Djw/aLTNVESF89oe15dkrUErtcaF413MC1Ld5lTCaJLHLGqDKY69e02YwRuxW7jY qarpt7k7aR5FbcfO5r4V/FK/Gvp4Dmoky8uap7SJIW6x -----END CERTIFICATE----- ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1744063413.0 acme-4.0.0/acme/_internal/tests/testdata/rsa1024_key.pem0000664000175000017500000000156714775045665022036 0ustar00willgwillg-----BEGIN RSA PRIVATE KEY----- MIICXAIBAAKBgQCaifO0fGlcAcjjcYEAPYcIL0Hf0KiNa9VCJ14RBdlZxLWRrVFi 4tdNCKSKqzKuKrrA8DWd4PHFD7UpLyRrPPXY6GozAyCT+5UFBClGJ2KyNKu+eU6/ w4C1kpO4lpeXs8ptFc1lA9P8V1M/MkWzTE402nPNK0uUmZNo2tsFpGJUSQIDAQAB AoGAFjLWxQhSAhtnhfRZ+XTdHrnbFpFchOQGgDgzdPKIJDLzefeRh0jacIBbUmgB Ia+Vn/1hVkpnsEzvUvkonBbnoYWlYVQdpNTmrrew7SOztf8/1fYCsSkyDAvqGTXc TmHM0PaLS+junoWcKOvQRVb0N3k+43OnBkr2b393Sx30qGECQQDNO2IBWOsYs8cB CZQAZs8zBlbwBFZibqovqpLwXt9adBIsT9XzgagGbJMpzSuoHTUn3QqqJd9uHD8X UTmmoh4NAkEAwMRauo+PlNj8W1lusflko52KL17+E5cmeOERM2xvhZNpO7d3/1ak Co9dxVMicrYSh7jXbcXFNt3xNDTv6Dg8LQJAPuJwMDt/pc0IMCAwMkNOP7M0lkyt 73E7QmnAplhblcq0+tDnnLpgsr84BHnyY4u3iuRm7SW3pXSQPGPOB2nrTQJANBXa HgakWSe4KEal7ljgpITwzZPxOwHgV1EZALgP+hu2l3gfaFLUyDWstKCd8jjYEOwU 6YhCnWyiu+SB3lEzkQJBAJapJpfypFyY8kQNYlYILLBcPu5fmy3QUZKHJ4L3rIVJ c2UTLMeBBgGFHT04CtWntmjwzSv+V6lwiCxKXsIUySc= -----END RSA PRIVATE KEY----- ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1744063413.0 acme-4.0.0/acme/_internal/tests/testdata/rsa2048_cert.pem0000664000175000017500000000241614775045665022204 0ustar00willgwillg-----BEGIN CERTIFICATE----- MIIDjjCCAnagAwIBAgIJALVG/VbBb5U7MA0GCSqGSIb3DQEBCwUAMFsxCzAJBgNV BAYTAkFVMQswCQYDVQQIDAJXQTEeMBwGA1UEBwwVVGhlIG1pZGRsZSBvZiBub3do ZXJlMR8wHQYDVQQKDBZDZXJ0Ym90IFRlc3QgQ2VydHMgSW5jMCAXDTE2MTEyODIx MzUzN1oYDzIyOTAwOTEzMjEzNTM3WjBbMQswCQYDVQQGEwJBVTELMAkGA1UECAwC V0ExHjAcBgNVBAcMFVRoZSBtaWRkbGUgb2Ygbm93aGVyZTEfMB0GA1UECgwWQ2Vy dGJvdCBUZXN0IENlcnRzIEluYzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoC ggEBANoVT1pdvRUUBOqvm7M2ebLEHV7higUH7qAGUZEkfP6W4YriYVY+IHrH1svN PSa+oPTK7weDNmT11ehWnGyECIM9z2r2Hi9yVV0ycxh4hWQ4Nt8BAKZwCwaXpyWm 7Gj6m2EzpSN5Dd67g5YAQBrUUh1+RRbFi9c0Ls/6ZOExMvfg8kqt4c2sXCgH1IFn xvvOjBYop7xh0x3L1Akyax0tw8qgQp/z5mkupmVDNJYPFmbzFPMNyDR61ed6QUTD g7P4UAuFkejLLzFvz5YaO7vC+huaTuPhInAhpzqpr4yU97KIjos2/83Itu/Cv8U1 RAeEeRTkh0WjUfltoem/5f8bIdsCAwEAAaNTMFEwHQYDVR0OBBYEFHy+bEYqwvFU uQLTkIfQ36AM2DQiMB8GA1UdIwQYMBaAFHy+bEYqwvFUuQLTkIfQ36AM2DQiMA8G A1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQELBQADggEBAH3ANVzB59FcunZV/F8T RiCD6/gV7Jc3CswU8N8tVjzMCg2jOdTFF9iYZzNNKQvG13o/n5LkQr/lkKRQkWTx nkE5WZbR7vNqlzXgPa9NBiK5rPjgSt8azPW+Skct3Bj4B3PhTMSpoQ7PsUJ8UeV8 kTNR5xrRLt6/mLfRJTXWXBM43GEZi8lL5q0nqz0tPGISADshHMo6ZlUu5Hvfp5v+ aonpO4sVS9hGOVxjGNMXYApEUy4jid9jjAfEk6jeELJMbXGLy/botFgIJK/QPe6P AfbdFgtg/qzG7Uy0A1iXXfWdgwmVrhCoGYYWCn4yWCAm894QKtdim87CHSDP0WUf Esg= -----END CERTIFICATE----- ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1744063413.0 acme-4.0.0/acme/_internal/tests/testdata/rsa2048_key.pem0000664000175000017500000000325014775045665022034 0ustar00willgwillg-----BEGIN PRIVATE KEY----- MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQDaFU9aXb0VFATq r5uzNnmyxB1e4YoFB+6gBlGRJHz+luGK4mFWPiB6x9bLzT0mvqD0yu8HgzZk9dXo VpxshAiDPc9q9h4vclVdMnMYeIVkODbfAQCmcAsGl6clpuxo+pthM6UjeQ3eu4OW AEAa1FIdfkUWxYvXNC7P+mThMTL34PJKreHNrFwoB9SBZ8b7zowWKKe8YdMdy9QJ MmsdLcPKoEKf8+ZpLqZlQzSWDxZm8xTzDcg0etXnekFEw4Oz+FALhZHoyy8xb8+W Gju7wvobmk7j4SJwIac6qa+MlPeyiI6LNv/NyLbvwr/FNUQHhHkU5IdFo1H5baHp v+X/GyHbAgMBAAECggEAURFe4C68XRuGAF+rN2Fmt+djK6QXlGswb1gp9hRkSpd3 3BLvMAoENOAYnsX6l26Bkr3lQRurmrgv/iBEIaqrJ25QrmgzLFwKE4zvcAdNPsYO z7MltLktwBOb1MlKVHPkUqvKFXeoikWWUqphKhgHNmN7900UALmrNTDVU0jgs3fB o35o8d5SjoC52K4wCTjhPyjt4cdbfbziRs2qFhfGdawidRO1xLlDM4tTTW+5yWGK lt0SwyvDVC6XWeNoT3nXyKjXWP7hcYqm0iS7ffL9YzEC2RXNGQUqeR50i9Y0rDdH Vqcr+Rqio2ww68zbDWBpC/jU133BSoHuSE1wstxIkQKBgQDxlEr42WJfgdajbZ1a hUIeLEgvhezLmD1hcYwZuQCLgizmY2ovvmeAH74koCDEsUUQunPYHsRla7wT3q1/ IkR1KgJPwESpkQaKuAqxeEAkv7Gn8Lzcn22jCoRCfGA68wKJz2ECFZDc0RDvRrT/ 9GhiiGUoO47jv9ezrSDO1eu5/QKBgQDnGfYVMNLiA0fy4AxSyY2vdo7vruOFGpRP n94gwxZ+0dQDWHzn3J4rHivxtcyd/MOZv4I8PtYK7tmmjYv1ngQ6sGl4p8bpUtwj 9++/B1CyB1W5/VPqMkd+Sj0dbejycME55+F6/r4basPXxBFFCfknjAlVvyvbBhUy ftNpHxZGtwKBgChJM4t2LPqCW3nbgL8ks9b2SX9rVQbKt4m1dsifWmDpb3VoJMAb f4UVRg8ziONkMIFOppzm3JeRNMcXflVSMJpdTA9in9CrN60QbfAUfpXiRc0cz1H3 YEAtM8smlKGf/s9efu3rDMJWNv3AC9UXPAUae8wOypBeYKk8+NilQe89AoGAXEA3 xFO+CqyGnwQixzVf0qf//NuSRQLMK1DEyc02gJ9gA4niKmgd11Zu8kjBClvo9MnG wifPJ4Qa6+pa8UwHoinjoF9Q/rit2cnSMS5JXxegd+MRCU7SzS3zYXkLYSPzbhsL Hh7sYmNnFA1XW3jUtZ2n6EusxPyTn5mS6MaZDNcCgYBelFKFjNIQ50NbOnm8DewK jUd5OFKowKodlQVcHiF9CVbjvpN8ZPRcBSmqDU4kpT/rmcybVoL6Zfa/zWkw8+Oh QxKb3BYf5vRUMd/RA+/t5KG4ZOIIYB3qoltAYlhVaINukL6cGVG1qvV/ntcsfsn6 kmf1UgGFcKrJuXgwEtTVxw== -----END PRIVATE KEY----- ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1744063413.0 acme-4.0.0/acme/_internal/tests/testdata/rsa256_key.pem0000664000175000017500000000045214775045665021754 0ustar00willgwillg-----BEGIN RSA PRIVATE KEY----- MIGrAgEAAiEAm2Fylv+Uz7trgTW8EBHP3FQSMeZs2GNQ6VRo1sIVJEkCAwEAAQIh AJT0BA/xD01dFCAXzSNyj9nfSZa3NpqzJZZn/eOm7vghAhEAzUVNZn4lLLBD1R6N E8TKNQIRAMHHyn3O5JeY36lwKwkUlEUCEAliRauN0L0+QZuYjfJ9aJECEGx4dru3 rTPCyighdqWNlHUCEQCiLjlwSRtWgmMBudCkVjzt -----END RSA PRIVATE KEY----- ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1744063413.0 acme-4.0.0/acme/_internal/tests/testdata/rsa4096_cert.pem0000664000175000017500000000342614775045665022213 0ustar00willgwillg-----BEGIN CERTIFICATE----- MIIFDTCCAvWgAwIBAgIUImqDrP53V69vFROsjP/gL0YtoA4wDQYJKoZIhvcNAQEL BQAwFjEUMBIGA1UEAwwLZXhhbXBsZS5jb20wHhcNMjAwNTI3MjMyNDE0WhcNMjAw NjI2MjMyNDE0WjAWMRQwEgYDVQQDDAtleGFtcGxlLmNvbTCCAiIwDQYJKoZIhvcN AQEBBQADggIPADCCAgoCggIBANY9LKLk9Dxn0MUMQFHwBoTN4ehDSWBws2KcytpF mc8m9Mfk1wmb4fQSKYtK3wIFMfIyo9HQu0nKqMkkUw52o3ZXyOv+oWwF5qNy2BKu lh5OMSkaZ0o13zoPpW42e+IUnyxvg70+0urD+sUue4cyTHh/nBIUjrM/05ZJ/ac8 HR0RK3H41YoqBjq69JjMZczZZhbNFit3s6p0R1TbVAgc3ckqbtX5BDyQMQQCP4Ed m4DgbAFVqdcPUCC5W3F3fmuQiPKHiADzONZnXpy6lUvLDWqcd6loKp+nKHM6OkXX 8hmD7pE1PYMQo4hqOfhBR2IgMjAShwd5qUFjl1m2oo0Qm3PFXOk6i2ZQdS6AA/yd B5/mX0RnM2oIdFZPb6UZFSmtEgs9sTzn+hMUyNSZQRE54px1ur1xws2R+vbsCyM5 +KoFVxDjVjU9TlZx3GvDvnqz/tbHjji6l8VHZYOBMBUXbKHu2U6pJFZ5Zp7k68/z a3Fb9Pjtn3iRkXEyC0N5kLgqO4QTlExnxebV8aMvQpWd/qefnMn9qPYIZPEXSQAR mEBIahkcACb60s+acG0WFFluwBPtBqEr8Q67XlSF0Ibf4iBiRzpPobhlWta1nrFg 4IWHMSoZ0PE75bhIGBEkhrpcXQCAxXmAfxfjKDH7jdJ1fRdnZ/9+OzwYGVX5GH/l 0QDtAgMBAAGjUzBRMB0GA1UdDgQWBBQh3xiz/o1nEU2ySylZ9gxCXvIPGzAfBgNV HSMEGDAWgBQh3xiz/o1nEU2ySylZ9gxCXvIPGzAPBgNVHRMBAf8EBTADAQH/MA0G CSqGSIb3DQEBCwUAA4ICAQAELoXz31oR9pdAwidlv9ZBOKiC7KBWy8VMqXNVkfTn bVRxAUex7zleLFIOkWnqadsMesU9sIwrbLzBcZ8Q/vBY+z2xOPdXcgcAoAmdKWoq YBQNiqng9r54sqlzB/77QZCf5fdktESe7NTxhCifgx5SAWq7IUQs/lm3tnMUSAfE 5ctuN6M+w8K54y3WDprcfMHpnc3ZHeSPhVQApHM0h/bDvXq0bRS7kmq27Hb153Qm nH3TwYB5pPSWW38NbUc+s/a7mItO7S8ly8yGbA0j9c/IbN5lM+OCdk06asz3+c8E uo8nuCBoYO5+6AqC2N7WJ3Tdr/pFA8jTbd6VNVlgCWTIR8ZosL5Fgkfv+4fUBrHt zdVUqMUzvga5rvZnwnJ5Qfu/drHeAAo9MTNFQNe2QgDlYfWBh5GweolgmFSwrpkY v/5wLtIyv/ASHKswybbqMIlpttcLTXjx5yuh8swttT6Wh+FQqqQ32KSRB3StiwyK oH0ZhrwYHiFYNlPxecGX6XUta6rFtTlEdkBGSnXzgiTzL2l+Nc0as0V5B9RninZG qJ+VOChSQ0OFvg1riSXv7tMvbLdGQnxwTRL3t6BMS8I4LA2m3ZfWUcuXT783ODTH 16f1Q1AgXd2csstTWO9cv+N/0fpX31nqrm6+CrGduSr2u4HjYYnlLIUhmdTvK3fX Fg== -----END CERTIFICATE----- ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1744063413.0 acme-4.0.0/acme/_internal/tests/testdata/rsa4096_key.pem0000664000175000017500000000625714775045665022053 0ustar00willgwillg-----BEGIN RSA PRIVATE KEY----- MIIJKgIBAAKCAgEA1j0souT0PGfQxQxAUfAGhM3h6ENJYHCzYpzK2kWZzyb0x+TX CZvh9BIpi0rfAgUx8jKj0dC7ScqoySRTDnajdlfI6/6hbAXmo3LYEq6WHk4xKRpn SjXfOg+lbjZ74hSfLG+DvT7S6sP6xS57hzJMeH+cEhSOsz/Tlkn9pzwdHRErcfjV iioGOrr0mMxlzNlmFs0WK3ezqnRHVNtUCBzdySpu1fkEPJAxBAI/gR2bgOBsAVWp 1w9QILlbcXd+a5CI8oeIAPM41mdenLqVS8sNapx3qWgqn6coczo6RdfyGYPukTU9 gxCjiGo5+EFHYiAyMBKHB3mpQWOXWbaijRCbc8Vc6TqLZlB1LoAD/J0Hn+ZfRGcz agh0Vk9vpRkVKa0SCz2xPOf6ExTI1JlBETninHW6vXHCzZH69uwLIzn4qgVXEONW NT1OVnHca8O+erP+1seOOLqXxUdlg4EwFRdsoe7ZTqkkVnlmnuTrz/NrcVv0+O2f eJGRcTILQ3mQuCo7hBOUTGfF5tXxoy9ClZ3+p5+cyf2o9ghk8RdJABGYQEhqGRwA JvrSz5pwbRYUWW7AE+0GoSvxDrteVIXQht/iIGJHOk+huGVa1rWesWDghYcxKhnQ 8TvluEgYESSGulxdAIDFeYB/F+MoMfuN0nV9F2dn/347PBgZVfkYf+XRAO0CAwEA AQKCAgEA0hZdTkQtCYtYm9LexDsXeWYX8VcCfrMmBj7xYcg9A3oVMmzDPuYBVwH0 gWbjd6y2hOaJ5TfGYZ99kvmvBRDsTSHaoyopC7BhssjtAKz6Ay/0X3VH8usPQ3WS aZi+NT65tK6KRqtz08ppgLGLa1G00bl5x/Um1rpxeACI4FU/y4BJ1VMJvJpnT3KE Z86Qyagqx5NH+UpCApZSWPFX3zjHePzGgcfXErjniCHYOnpZQrFQ2KIzkfSvQ9fg x01ByKOM2CB2C1B33TCzBAioXRH6zyAu7A59NeCK9ywTduhDvie1a+oEryFC7IQW 4s7I/H3MGX4hsf/pLXlHMy+5CZJOjRaC2h+pypfbbcuiXu6Sn64kHNpiI7SxI5DI MIRjyG7MdUcrzq0Rt8ogwwpbCoRqrl/w3bhxtqmeZaEZtyxbjlm7reK2YkIFDgyz JMqiJK5ZAi+9L/8c0xhjjAQQ0sIzrjmjA8U+6YnWL9jU5qXTVnBB8XQucyeeZGgk yRHyMur71qOXN8z3UEva7MHkDTUBlj8DgTz6sEjqCipaWl0CXfDNa4IhHIXD5qiF wplhq7OeS0v6EGG/UFa3Q/lFntxtrayxJX7uvvSccGzjPKXTjpWUELLi/FdnIsum eXT3RgIEYozj4BibDXaBLfHTCVzxOr7AAEvKM9XWSUgLA0paSWECggEBAO9ZBeE1 GWzd1ejTTkcxBC9AK2rNsYG8PdNqiof/iTbuJWNeRqpG+KB/0CNIpjZ2X5xZd0tM FDpHTFehlP26Roxuq50iRAFc+SN5KoiO0A3JuJAidreIgRTia1saUUrypHqWrYEA VZVj2AI8Pyg3s1OkR2frFskY7hXBVb/pJNDP/m9xTXXIYiIXYkHYe+4RIJCnAxRv q5YHKaX+0Ull9YCZJCxmwvcHat8sgu8qkiwUMEM6QSNEkrEbdnWYBABvC1AR6sws 7MP1h9+j22n4Zc/3D6kpFZEL9Erx8nNyhbOZ6q2Tdnf6YKVVjZdyVa8VyNnR0ROl 3BjkFaHb/bg4e4kCggEBAOUk8ZJS3qBeGCOjug384zbHGcnhUBYtYJiOz+RXBtP+ PRksbFtTkgk1sHuSGO8YRddU4Qv7Av1xL8o+DEsLBSD0YQ7pmLrR/LK+iDQ5N63O Fve9uJH0ybxAOkiua7G24+lTsVUP//KWToL4Wh5zbHBBjL5D2Z9zoeVbcE87xhva lImMVr4Ex252DqNP9wkZxBjudFyJ/C/TnXrjPcgwhxWTC7sLQMhE5p+490G7c4hX PywkIKrANbu37KDiAvVS+dC66ZgpL/NUDkeloAmGNO08LGzbV6YKchlvDyWU/AvW 0hYjbL0FUq7K/wp1G9fumolB+fbI25K9c13X93STzUUCggEBAJDsNFUyk5yJjbYW C/WrRj9d+WwH9Az77+uNPSgvn+O0usq6EMuVgYGdImfa21lqv2Wp/kOHY1AOT7lX yyD+oyzw7dSNJOQ2aVwDR6+72Vof5DLRy1RBwPbmSd61xrc8yD658YCEtU1pUSe5 VvyBDYH9nIbdn8RP5gkiMUusXXBaIFNWJXLFzDWcNxBrhk6V7EPp/EFphFmpKJyr +AkbRVWCZJbF+hMdWKadCwLJogwyhS6PnVU/dhrq6AU38GRa2Fy5HJRYN1xH1Oej DX3Su8L6c28Xw0k6FcczTHx+wVoIPkKvYTIwVkiFzt/+iMckx6KsGo5tBSHFKRwC WlQrTxECggEBALjUruLnY1oZ7AC7bTUhOimSOfQEgTQSUCtebsRxijlvhtsKYTDd XRt+qidStjgN7S/+8DRYuZWzOeg5WnMhpXZqiOudcyume922IGl3ibjxVsdoyjs5 J4xohlrgDlBgBMDNWGoTqNGFejjcmNydH+gAh8VlN2INxJYbxqCyx17qVgwJHmLR uggYxD/pHYvCs9GkbknCp5/wYsOgDtKuihfV741lS1D/esN1UEQ+LrfYIEW7snno 5q7Pcdhn1hkKYCWEzy2Ec4Aj2gzixQ9JqOF/OxpnZvCw1k47rg0TeqcWFYnz8x8Y 7xO8/DH0OoxXk2GJzVXJuItJs4gLzzfCjL0CggEAJFHfC9jisdy7CoWiOpNCSF1B S0/CWDz77cZdlWkpTdaXGGp1MA/UKUFPIH8sOHfvpKS660+X4G/1ZBHmFb4P5kFF Qy8UyUMKtSOEdZS6KFlRlfSCAMd5aSTmCvq4OSjYEpMRwUhU/iEJNkn9Z1Soehe0 U3dxJ8KiT1071geO6rRquSHoSJs6Y0WQKriYYQJOhh4Axs3PQihER2eyh+WGk8YJ 02m0mMsjntqnXtdc6IcdKaHp9ko+OpM9QZLsvt19fxBcrXj/i21uUXrzuNtKfO6M JqGhsOrO2dh8lMhvodENvgKA0DmYDC9N7ogo7bxTNSedcjBF46FhJoqii8m70Q== -----END RSA PRIVATE KEY----- ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1744063413.0 acme-4.0.0/acme/_internal/tests/testdata/rsa512_key.pem0000664000175000017500000000075514775045665021755 0ustar00willgwillg-----BEGIN RSA PRIVATE KEY----- MIIBOgIBAAJBAKx1c7RR7R/drnBSQ/zfx1vQLHUbFLh1AQQQ5R8DZUXd36efNK79 vukFhN9HFoHZiUvOjm0c+pVE6K+EdE/twuUCAwEAAQJAMbrEnJCrQe8YqAbw1/Bn elAzIamndfE3U8bTavf9sgFpS4HL83rhd6PDbvx81ucaJAT/5x048fM/nFl4fzAc mQIhAOF/a9o3EIsDKEmUl+Z1OaOiUxDF3kqWSmALEsmvDhwXAiEAw8ljV5RO/rUp Zu2YMDFq3MKpyyMgBIJ8CxmGRc6gCmMCIGRQzkcmhfqBrhOFwkmozrqIBRIKJIjj 8TRm2LXWZZ2DAiAqVO7PztdNpynugUy4jtbGKKjBrTSNBRGA7OHlUgm0dQIhALQq 6oGU29Vxlvt3k0vmiRKU4AVfLyNXIGtcWcNG46h/ -----END RSA PRIVATE KEY----- ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1744063413.0 acme-4.0.0/acme/_internal/tests/util_test.py0000664000175000017500000000057414775045665020141 0ustar00willgwillg"""Tests for acme.util.""" import sys import pytest def test_it(): from acme.util import map_keys assert {'a': 'b', 'c': 'd'} == \ map_keys({'a': 'b', 'c': 'd'}, lambda key: key) assert {2: 2, 4: 4} == map_keys({1: 2, 3: 4}, lambda x: x + 1) if __name__ == '__main__': sys.exit(pytest.main(sys.argv[1:] + [__file__])) # pragma: no cover ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1744063413.0 acme-4.0.0/acme/challenges.py0000664000175000017500000005422414775045665015116 0ustar00willgwillg"""ACME Identifier Validation Challenges.""" import abc import functools import hashlib import logging import socket from typing import Any from typing import cast from typing import Dict from typing import Mapping from typing import Optional from typing import Tuple from typing import Type from typing import TypeVar from typing import Union from cryptography import x509 from cryptography.hazmat.primitives import hashes import josepy as jose from OpenSSL import crypto from OpenSSL import SSL import requests from acme import crypto_util from acme import errors logger = logging.getLogger(__name__) GenericChallenge = TypeVar('GenericChallenge', bound='Challenge') class Challenge(jose.TypedJSONObjectWithFields): # _fields_to_partial_json """ACME challenge.""" TYPES: Dict[str, Type['Challenge']] = {} @classmethod def from_json(cls: Type[GenericChallenge], jobj: Mapping[str, Any]) -> Union[GenericChallenge, 'UnrecognizedChallenge']: try: return cast(GenericChallenge, super().from_json(jobj)) except jose.UnrecognizedTypeError as error: logger.debug(error) return UnrecognizedChallenge.from_json(jobj) class ChallengeResponse(jose.TypedJSONObjectWithFields): # _fields_to_partial_json """ACME challenge response.""" TYPES: Dict[str, Type['ChallengeResponse']] = {} def to_partial_json(self) -> Dict[str, Any]: # Removes the `type` field which is inserted by TypedJSONObjectWithFields.to_partial_json. # This field breaks RFC8555 compliance. jobj = super().to_partial_json() jobj.pop(self.type_field_name, None) return jobj class UnrecognizedChallenge(Challenge): """Unrecognized challenge. ACME specification defines a generic framework for challenges and defines some standard challenges that are implemented in this module. However, other implementations (including peers) might define additional challenge types, which should be ignored if unrecognized. :ivar jobj: Original JSON decoded object. """ jobj: Dict[str, Any] def __init__(self, jobj: Mapping[str, Any]) -> None: super().__init__() object.__setattr__(self, "jobj", jobj) def to_partial_json(self) -> Dict[str, Any]: return self.jobj # pylint: disable=no-member @classmethod def from_json(cls, jobj: Mapping[str, Any]) -> 'UnrecognizedChallenge': return cls(jobj) class _TokenChallenge(Challenge): """Challenge with token. :ivar bytes token: """ TOKEN_SIZE = 128 // 8 # Based on the entropy value from the spec """Minimum size of the :attr:`token` in bytes.""" # TODO: acme-spec doesn't specify token as base64-encoded value token: bytes = jose.field( "token", encoder=jose.encode_b64jose, decoder=functools.partial( jose.decode_b64jose, size=TOKEN_SIZE, minimum=True)) # XXX: rename to ~token_good_for_url @property def good_token(self) -> bool: # XXX: @token.decoder """Is `token` good? .. todo:: acme-spec wants "It MUST NOT contain any non-ASCII characters", but it should also warrant that it doesn't contain ".." or "/"... """ # TODO: check that path combined with uri does not go above # URI_ROOT_PATH! # pylint: disable=unsupported-membership-test return b'..' not in self.token and b'/' not in self.token class KeyAuthorizationChallengeResponse(ChallengeResponse): """Response to Challenges based on Key Authorization. :param str key_authorization: """ key_authorization: str = jose.field("keyAuthorization") thumbprint_hash_function = hashes.SHA256 def verify(self, chall: 'KeyAuthorizationChallenge', account_public_key: jose.JWK) -> bool: """Verify the key authorization. :param KeyAuthorization chall: Challenge that corresponds to this response. :param JWK account_public_key: :return: ``True`` iff verification of the key authorization was successful. :rtype: bool """ parts = self.key_authorization.split('.') # pylint: disable=no-member if len(parts) != 2: logger.debug("Key authorization (%r) is not well formed", self.key_authorization) return False if parts[0] != chall.encode("token"): logger.debug("Mismatching token in key authorization: " "%r instead of %r", parts[0], chall.encode("token")) return False thumbprint = jose.b64encode(account_public_key.thumbprint( hash_function=self.thumbprint_hash_function)).decode() if parts[1] != thumbprint: logger.debug("Mismatching thumbprint in key authorization: " "%r instead of %r", parts[0], thumbprint) return False return True def to_partial_json(self) -> Dict[str, Any]: jobj = super().to_partial_json() jobj.pop('keyAuthorization', None) return jobj # TODO: Make this method a generic of K (bound=KeyAuthorizationChallenge), response_cls of type # Type[K] and use it in response/response_and_validation return types once Python 3.6 support is # dropped (do not support generic ABC classes, see https://github.com/python/typing/issues/449). class KeyAuthorizationChallenge(_TokenChallenge, metaclass=abc.ABCMeta): """Challenge based on Key Authorization. :param response_cls: Subclass of `KeyAuthorizationChallengeResponse` that will be used to generate ``response``. :param str typ: type of the challenge """ typ: str = NotImplemented response_cls: Type[KeyAuthorizationChallengeResponse] = NotImplemented thumbprint_hash_function = ( KeyAuthorizationChallengeResponse.thumbprint_hash_function) def key_authorization(self, account_key: jose.JWK) -> str: """Generate Key Authorization. :param JWK account_key: :rtype str: """ return self.encode("token") + "." + jose.b64encode( account_key.thumbprint( hash_function=self.thumbprint_hash_function)).decode() def response(self, account_key: jose.JWK) -> KeyAuthorizationChallengeResponse: """Generate response to the challenge. :param JWK account_key: :returns: Response (initialized `response_cls`) to the challenge. :rtype: KeyAuthorizationChallengeResponse """ return self.response_cls( # pylint: disable=not-callable key_authorization=self.key_authorization(account_key)) @abc.abstractmethod def validation(self, account_key: jose.JWK, **kwargs: Any) -> Any: """Generate validation for the challenge. Subclasses must implement this method, but they are likely to return completely different data structures, depending on what's necessary to complete the challenge. Interpretation of that return value must be known to the caller. :param JWK account_key: :returns: Challenge-specific validation. """ raise NotImplementedError() # pragma: no cover def response_and_validation(self, account_key: jose.JWK, *args: Any, **kwargs: Any ) -> Tuple[KeyAuthorizationChallengeResponse, Any]: """Generate response and validation. Convenience function that return results of `response` and `validation`. :param JWK account_key: :rtype: tuple """ return (self.response(account_key), self.validation(account_key, *args, **kwargs)) @ChallengeResponse.register class DNS01Response(KeyAuthorizationChallengeResponse): """ACME dns-01 challenge response.""" typ = "dns-01" def simple_verify(self, chall: 'DNS01', domain: str, account_public_key: jose.JWK) -> bool: # pylint: disable=unused-argument """Simple verify. This method no longer checks DNS records and is a simple wrapper around `KeyAuthorizationChallengeResponse.verify`. :param challenges.DNS01 chall: Corresponding challenge. :param str domain: Domain name being verified. :param JWK account_public_key: Public key for the key pair being authorized. :return: ``True`` iff verification of the key authorization was successful. :rtype: bool """ verified = self.verify(chall, account_public_key) if not verified: logger.debug("Verification of key authorization in response failed") return verified @Challenge.register class DNS01(KeyAuthorizationChallenge): """ACME dns-01 challenge.""" response_cls = DNS01Response typ = response_cls.typ LABEL = "_acme-challenge" """Label clients prepend to the domain name being validated.""" def validation(self, account_key: jose.JWK, **unused_kwargs: Any) -> str: """Generate validation. :param JWK account_key: :rtype: str """ return jose.b64encode(hashlib.sha256(self.key_authorization( account_key).encode("utf-8")).digest()).decode() def validation_domain_name(self, name: str) -> str: """Domain name for TXT validation record. :param str name: Domain name being validated. :rtype: str """ return f"{self.LABEL}.{name}" @ChallengeResponse.register class HTTP01Response(KeyAuthorizationChallengeResponse): """ACME http-01 challenge response.""" typ = "http-01" PORT = 80 """Verification port as defined by the protocol. You can override it (e.g. for testing) by passing ``port`` to `simple_verify`. """ WHITESPACE_CUTSET = "\n\r\t " """Whitespace characters which should be ignored at the end of the body.""" def simple_verify(self, chall: 'HTTP01', domain: str, account_public_key: jose.JWK, port: Optional[int] = None, timeout: int = 30) -> bool: """Simple verify. :param challenges.SimpleHTTP chall: Corresponding challenge. :param str domain: Domain name being verified. :param JWK account_public_key: Public key for the key pair being authorized. :param int port: Port used in the validation. :param int timeout: Timeout in seconds. :returns: ``True`` iff validation with the files currently served by the HTTP server is successful. :rtype: bool """ if not self.verify(chall, account_public_key): logger.debug("Verification of key authorization in response failed") return False # TODO: ACME specification defines URI template that doesn't # allow to use a custom port... Make sure port is not in the # request URI, if it's standard. if port is not None and port != self.PORT: logger.warning( "Using non-standard port for http-01 verification: %s", port) domain += ":{0}".format(port) uri = chall.uri(domain) logger.debug("Verifying %s at %s...", chall.typ, uri) try: http_response = requests.get(uri, verify=False, timeout=timeout) except requests.exceptions.RequestException as error: logger.error("Unable to reach %s: %s", uri, error) return False # By default, http_response.text will try to guess the encoding to use # when decoding the response to Python unicode strings. This guesswork # is error prone. RFC 8555 specifies that HTTP-01 responses should be # key authorizations with possible trailing whitespace. Since key # authorizations must be composed entirely of the base64url alphabet # plus ".", we tell requests that the response should be ASCII. See # https://datatracker.ietf.org/doc/html/rfc8555#section-8.3 for more # info. http_response.encoding = "ascii" logger.debug("Received %s: %s. Headers: %s", http_response, http_response.text, http_response.headers) challenge_response = http_response.text.rstrip(self.WHITESPACE_CUTSET) if self.key_authorization != challenge_response: logger.debug("Key authorization from response (%r) doesn't match " "HTTP response (%r)", self.key_authorization, challenge_response) return False return True @Challenge.register class HTTP01(KeyAuthorizationChallenge): """ACME http-01 challenge.""" response_cls = HTTP01Response typ = response_cls.typ URI_ROOT_PATH = ".well-known/acme-challenge" """URI root path for the server provisioned resource.""" @property def path(self) -> str: """Path (starting with '/') for provisioned resource. :rtype: str """ return '/' + self.URI_ROOT_PATH + '/' + self.encode('token') def uri(self, domain: str) -> str: """Create an URI to the provisioned resource. Forms an URI to the HTTPS server provisioned resource (containing :attr:`~SimpleHTTP.token`). :param str domain: Domain name being verified. :rtype: str """ return "http://" + domain + self.path def validation(self, account_key: jose.JWK, **unused_kwargs: Any) -> str: """Generate validation. :param JWK account_key: :rtype: str """ return self.key_authorization(account_key) @ChallengeResponse.register class TLSALPN01Response(KeyAuthorizationChallengeResponse): """ACME tls-alpn-01 challenge response.""" typ = "tls-alpn-01" PORT = 443 """Verification port as defined by the protocol. You can override it (e.g. for testing) by passing ``port`` to `simple_verify`. """ ID_PE_ACME_IDENTIFIER_V1 = b"1.3.6.1.5.5.7.1.30.1" ACME_TLS_1_PROTOCOL = b"acme-tls/1" @property def h(self) -> bytes: """Hash value stored in challenge certificate""" return hashlib.sha256(self.key_authorization.encode('utf-8')).digest() def gen_cert(self, domain: str, key: Optional[crypto.PKey] = None, bits: int = 2048 ) -> Tuple[x509.Certificate, crypto.PKey]: """Generate tls-alpn-01 certificate. :param str domain: Domain verified by the challenge. :param OpenSSL.crypto.PKey key: Optional private key used in certificate generation. If not provided (``None``), then fresh key will be generated. :param int bits: Number of bits for newly generated key. :rtype: `tuple` of `x509.Certificate` and `OpenSSL.crypto.PKey` """ if key is None: key = crypto.PKey() key.generate_key(crypto.TYPE_RSA, bits) oid = x509.ObjectIdentifier(self.ID_PE_ACME_IDENTIFIER_V1.decode()) acme_extension = x509.Extension( oid, critical=True, value=x509.UnrecognizedExtension(oid, self.h) ) cryptography_key = key.to_cryptography_key() assert isinstance(cryptography_key, crypto_util.CertificateIssuerPrivateKeyTypesTpl) cert = crypto_util.make_self_signed_cert( cryptography_key, [domain], force_san=True, extensions=[acme_extension] ) return cert, key def probe_cert(self, domain: str, host: Optional[str] = None, port: Optional[int] = None) -> x509.Certificate: """Probe tls-alpn-01 challenge certificate. :param str domain: domain being validated, required. :param str host: IP address used to probe the certificate. :param int port: Port used to probe the certificate. """ if host is None: host = socket.gethostbyname(domain) logger.debug('%s resolved to %s', domain, host) if port is None: port = self.PORT return crypto_util.probe_sni(host=host.encode(), port=port, name=domain.encode(), alpn_protocols=[self.ACME_TLS_1_PROTOCOL]) def verify_cert(self, domain: str, cert: x509.Certificate, ) -> bool: """Verify tls-alpn-01 challenge certificate. :param str domain: Domain name being validated. :param cert: Challenge certificate. :type cert: `cryptography.x509.Certificate` :returns: Whether the certificate was successfully verified. :rtype: bool """ names = crypto_util.get_names_from_subject_and_extensions( cert.subject, cert.extensions ) logger.debug( "Certificate %s. SANs: %s", cert.fingerprint(hashes.SHA256()), names ) if len(names) != 1 or names[0].lower() != domain.lower(): return False try: ext = cert.extensions.get_extension_for_oid( x509.ObjectIdentifier(self.ID_PE_ACME_IDENTIFIER_V1.decode()) ) except x509.ExtensionNotFound: return False # This is for the type checker. assert isinstance(ext.value, x509.UnrecognizedExtension) return ext.value.value == self.h # pylint: disable=too-many-arguments def simple_verify(self, chall: 'TLSALPN01', domain: str, account_public_key: jose.JWK, cert: Optional[x509.Certificate] = None, host: Optional[str] = None, port: Optional[int] = None) -> bool: """Simple verify. Verify ``validation`` using ``account_public_key``, optionally probe tls-alpn-01 certificate and check using `verify_cert`. :param .challenges.TLSALPN01 chall: Corresponding challenge. :param str domain: Domain name being validated. :param JWK account_public_key: :param x509.Certificate cert: Optional certificate. If not provided (``None``) certificate will be retrieved using `probe_cert`. :param string host: IP address used to probe the certificate. :param int port: Port used to probe the certificate. :returns: ``True`` if and only if client's control of the domain has been verified. :rtype: bool """ if not self.verify(chall, account_public_key): logger.debug("Verification of key authorization in response failed") return False if cert is None: try: cert = self.probe_cert(domain=domain, host=host, port=port) except errors.Error as error: logger.debug(str(error), exc_info=True) return False return self.verify_cert(domain, cert) @Challenge.register # pylint: disable=too-many-ancestors class TLSALPN01(KeyAuthorizationChallenge): """ACME tls-alpn-01 challenge.""" response_cls = TLSALPN01Response typ = response_cls.typ def validation(self, account_key: jose.JWK, **kwargs: Any) -> Tuple[x509.Certificate, crypto.PKey]: """Generate validation. :param JWK account_key: :param str domain: Domain verified by the challenge. :param OpenSSL.crypto.PKey cert_key: Optional private key used in certificate generation. If not provided (``None``), then fresh key will be generated. :rtype: `tuple` of `x509.Certificate` and `OpenSSL.crypto.PKey` """ # TODO: Remove cast when response() is generic. return cast(TLSALPN01Response, self.response(account_key)).gen_cert( key=kwargs.get('cert_key'), domain=cast(str, kwargs.get('domain'))) @staticmethod def is_supported() -> bool: """ Check if TLS-ALPN-01 challenge is supported on this machine. This implies that a recent version of OpenSSL is installed (>= 1.0.2), or a recent cryptography version shipped with the OpenSSL library is installed. :returns: ``True`` if TLS-ALPN-01 is supported on this machine, ``False`` otherwise. :rtype: bool """ return (hasattr(SSL.Connection, "set_alpn_protos") and hasattr(SSL.Context, "set_alpn_select_callback")) @Challenge.register class DNS(_TokenChallenge): """ACME "dns" challenge.""" typ = "dns" LABEL = "_acme-challenge" """Label clients prepend to the domain name being validated.""" def gen_validation(self, account_key: jose.JWK, alg: jose.JWASignature = jose.RS256, **kwargs: Any) -> jose.JWS: """Generate validation. :param .JWK account_key: Private account key. :param .JWA alg: :returns: This challenge wrapped in `.JWS` :rtype: .JWS """ return jose.JWS.sign( payload=self.json_dumps(sort_keys=True).encode('utf-8'), key=account_key, alg=alg, **kwargs) def check_validation(self, validation: jose.JWS, account_public_key: jose.JWK) -> bool: """Check validation. :param JWS validation: :param JWK account_public_key: :rtype: bool """ if not validation.verify(key=account_public_key): return False try: return self == self.json_loads( validation.payload.decode('utf-8')) except jose.DeserializationError as error: logger.debug("Checking validation for DNS failed: %s", error) return False def gen_response(self, account_key: jose.JWK, **kwargs: Any) -> 'DNSResponse': """Generate response. :param .JWK account_key: Private account key. :param .JWA alg: :rtype: DNSResponse """ return DNSResponse(validation=self.gen_validation(account_key, **kwargs)) def validation_domain_name(self, name: str) -> str: """Domain name for TXT validation record. :param str name: Domain name being validated. """ return "{0}.{1}".format(self.LABEL, name) @ChallengeResponse.register class DNSResponse(ChallengeResponse): """ACME "dns" challenge response. :param JWS validation: """ typ = "dns" validation: jose.JWS = jose.field("validation", decoder=jose.JWS.from_json) def check_validation(self, chall: 'DNS', account_public_key: jose.JWK) -> bool: """Check validation. :param challenges.DNS chall: :param JWK account_public_key: :rtype: bool """ return chall.check_validation(self.validation, account_public_key) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1744063413.0 acme-4.0.0/acme/client.py0000664000175000017500000007471514775045665014276 0ustar00willgwillg"""ACME client API.""" import base64 import datetime from email.utils import parsedate_tz import http.client as http_client import logging import re import time from typing import Any from typing import cast from typing import List from typing import Mapping from typing import Optional from typing import Set from typing import Tuple from typing import Union from cryptography import x509 import josepy as jose import requests from requests.adapters import HTTPAdapter from requests.utils import parse_header_links from acme import challenges from acme import crypto_util from acme import errors from acme import jws from acme import messages logger = logging.getLogger(__name__) DEFAULT_NETWORK_TIMEOUT = 45 class ClientV2: """ACME client for a v2 API. :ivar messages.Directory directory: :ivar .ClientNetwork net: Client network. """ def __init__(self, directory: messages.Directory, net: 'ClientNetwork') -> None: """Initialize. :param .messages.Directory directory: Directory Resource :param .ClientNetwork net: Client network. """ self.directory = directory self.net = net def new_account(self, new_account: messages.NewRegistration) -> messages.RegistrationResource: """Register. :param .NewRegistration new_account: :raises .ConflictError: in case the account already exists :returns: Registration Resource. :rtype: `.RegistrationResource` """ response = self._post(self.directory['newAccount'], new_account) # if account already exists if response.status_code == 200 and 'Location' in response.headers: raise errors.ConflictError(response.headers['Location']) # "Instance of 'Field' has no key/contact member" bug: regr = self._regr_from_response(response) self.net.account = regr return regr def query_registration(self, regr: messages.RegistrationResource ) -> messages.RegistrationResource: """Query server about registration. :param messages.RegistrationResource regr: Existing Registration Resource. """ self.net.account = self._get_v2_account(regr, True) return self.net.account def update_registration(self, regr: messages.RegistrationResource, update: Optional[messages.Registration] = None ) -> messages.RegistrationResource: """Update registration. :param messages.RegistrationResource regr: Registration Resource. :param messages.Registration update: Updated body of the resource. If not provided, body will be taken from `regr`. :returns: Updated Registration Resource. :rtype: `.RegistrationResource` """ # https://github.com/certbot/certbot/issues/6155 regr = self._get_v2_account(regr) update = regr.body if update is None else update body = messages.UpdateRegistration(**dict(update)) updated_regr = self._send_recv_regr(regr, body=body) self.net.account = updated_regr return updated_regr def _get_v2_account(self, regr: messages.RegistrationResource, update_body: bool = False ) -> messages.RegistrationResource: self.net.account = None only_existing_reg = regr.body.update(only_return_existing=True) response = self._post(self.directory['newAccount'], only_existing_reg) updated_uri = response.headers['Location'] new_regr = regr.update(body=messages.Registration.from_json(response.json()) if update_body else regr.body, uri=updated_uri) self.net.account = new_regr return new_regr def new_order(self, csr_pem: bytes, profile: Optional[str] = None) -> messages.OrderResource: """Request a new Order object from the server. :param bytes csr_pem: A CSR in PEM format. :returns: The newly created order. :rtype: OrderResource """ csr = x509.load_pem_x509_csr(csr_pem) dnsNames = crypto_util.get_names_from_subject_and_extensions(csr.subject, csr.extensions) try: san_ext = csr.extensions.get_extension_for_class(x509.SubjectAlternativeName) except x509.ExtensionNotFound: ipNames = [] else: ipNames = san_ext.value.get_values_for_type(x509.IPAddress) identifiers = [] for name in dnsNames: identifiers.append(messages.Identifier(typ=messages.IDENTIFIER_FQDN, value=name)) for ip in ipNames: identifiers.append(messages.Identifier(typ=messages.IDENTIFIER_IP, value=str(ip))) if profile is None: profile = "" order = messages.NewOrder(identifiers=identifiers, profile=profile) response = self._post(self.directory['newOrder'], order) body = messages.Order.from_json(response.json()) authorizations = [] # pylint has trouble understanding our josepy based objects which use # things like custom metaclass logic. body.authorizations should be a # list of strings containing URLs so let's disable this check here. for url in body.authorizations: # pylint: disable=not-an-iterable authorizations.append(self._authzr_from_response(self._post_as_get(url), uri=url)) return messages.OrderResource( body=body, uri=response.headers.get('Location'), authorizations=authorizations, csr_pem=csr_pem) def poll(self, authzr: messages.AuthorizationResource ) -> Tuple[messages.AuthorizationResource, requests.Response]: """Poll Authorization Resource for status. :param authzr: Authorization Resource :type authzr: `.AuthorizationResource` :returns: Updated Authorization Resource and HTTP response. :rtype: (`.AuthorizationResource`, `requests.Response`) """ response = self._post_as_get(authzr.uri) updated_authzr = self._authzr_from_response( response, authzr.body.identifier, authzr.uri) return updated_authzr, response def poll_and_finalize(self, orderr: messages.OrderResource, deadline: Optional[datetime.datetime] = None) -> messages.OrderResource: """Poll authorizations and finalize the order. If no deadline is provided, this method will timeout after 90 seconds. :param messages.OrderResource orderr: order to finalize :param datetime.datetime deadline: when to stop polling and timeout :returns: finalized order :rtype: messages.OrderResource """ if deadline is None: deadline = datetime.datetime.now() + datetime.timedelta(seconds=90) orderr = self.poll_authorizations(orderr, deadline) return self.finalize_order(orderr, deadline) def poll_authorizations(self, orderr: messages.OrderResource, deadline: datetime.datetime ) -> messages.OrderResource: """Poll Order Resource for status.""" responses = [] for url in orderr.body.authorizations: while datetime.datetime.now() < deadline: authzr = self._authzr_from_response(self._post_as_get(url), uri=url) if authzr.body.status != messages.STATUS_PENDING: # pylint: disable=no-member responses.append(authzr) break time.sleep(1) # If we didn't get a response for every authorization, we fell through # the bottom of the loop due to hitting the deadline. if len(responses) < len(orderr.body.authorizations): raise errors.TimeoutError() failed = [] for authzr in responses: if authzr.body.status != messages.STATUS_VALID: for chall in authzr.body.challenges: if chall.error is not None: failed.append(authzr) if failed: raise errors.ValidationError(failed) return orderr.update(authorizations=responses) def begin_finalization(self, orderr: messages.OrderResource ) -> messages.OrderResource: """Start the process of finalizing an order. :param messages.OrderResource orderr: order to finalize :param datetime.datetime deadline: when to stop polling and timeout :returns: updated order :rtype: messages.OrderResource """ csr = x509.load_pem_x509_csr(orderr.csr_pem) wrapped_csr = messages.CertificateRequest(csr=csr) res = self._post(orderr.body.finalize, wrapped_csr) orderr = orderr.update(body=messages.Order.from_json(res.json())) return orderr def poll_finalization(self, orderr: messages.OrderResource, deadline: datetime.datetime, fetch_alternative_chains: bool = False ) -> messages.OrderResource: """ Poll an order that has been finalized for its status. If it becomes valid, obtain the certificate. :returns: finalized order (with certificate) :rtype: messages.OrderResource """ while datetime.datetime.now() < deadline: time.sleep(1) response = self._post_as_get(orderr.uri) body = messages.Order.from_json(response.json()) if body.status == messages.STATUS_INVALID: if body.error is not None: raise errors.IssuanceError(body.error) raise errors.Error( "The certificate order failed. No further information was provided " "by the server.") elif body.status == messages.STATUS_VALID and body.certificate is not None: certificate_response = self._post_as_get(body.certificate) orderr = orderr.update(body=body, fullchain_pem=certificate_response.text) if fetch_alternative_chains: alt_chains_urls = self._get_links(certificate_response, 'alternate') alt_chains = [self._post_as_get(url).text for url in alt_chains_urls] orderr = orderr.update(alternative_fullchains_pem=alt_chains) return orderr raise errors.TimeoutError() def finalize_order(self, orderr: messages.OrderResource, deadline: datetime.datetime, fetch_alternative_chains: bool = False) -> messages.OrderResource: """Finalize an order and obtain a certificate. :param messages.OrderResource orderr: order to finalize :param datetime.datetime deadline: when to stop polling and timeout :param bool fetch_alternative_chains: whether to also fetch alternative certificate chains :returns: finalized order :rtype: messages.OrderResource """ self.begin_finalization(orderr) return self.poll_finalization(orderr, deadline, fetch_alternative_chains) def revoke(self, cert: x509.Certificate, rsn: int) -> None: """Revoke certificate. :param x509.Certificate cert: `x509.Certificate` :param int rsn: Reason code for certificate revocation. :raises .ClientError: If revocation is unsuccessful. """ self._revoke(cert, rsn, self.directory['revokeCert']) def external_account_required(self) -> bool: """Checks if ACME server requires External Account Binding authentication.""" return hasattr(self.directory, 'meta') and \ hasattr(self.directory.meta, 'external_account_required') and \ self.directory.meta.external_account_required def _post_as_get(self, *args: Any, **kwargs: Any) -> requests.Response: """ Send GET request using the POST-as-GET protocol. :param args: :param kwargs: :return: """ new_args = args[:1] + (None,) + args[1:] return self._post(*new_args, **kwargs) def _get_links(self, response: requests.Response, relation_type: str) -> List[str]: """ Retrieves all Link URIs of relation_type from the response. :param requests.Response response: The requests HTTP response. :param str relation_type: The relation type to filter by. """ # Can't use response.links directly because it drops multiple links # of the same relation type, which is possible in RFC8555 responses. if 'Link' not in response.headers: return [] links = parse_header_links(response.headers['Link']) return [l['url'] for l in links if 'rel' in l and 'url' in l and l['rel'] == relation_type] @classmethod def get_directory(cls, url: str, net: 'ClientNetwork') -> messages.Directory: """ Retrieves the ACME directory (RFC 8555 section 7.1.1) from the ACME server. :param str url: the URL where the ACME directory is available :param ClientNetwork net: the ClientNetwork to use to make the request :returns: the ACME directory object :rtype: messages.Directory """ return messages.Directory.from_json(net.get(url).json()) @classmethod def _regr_from_response(cls, response: requests.Response, uri: Optional[str] = None, terms_of_service: Optional[str] = None ) -> messages.RegistrationResource: if 'terms-of-service' in response.links: terms_of_service = response.links['terms-of-service']['url'] return messages.RegistrationResource( body=messages.Registration.from_json(response.json()), uri=response.headers.get('Location', uri), terms_of_service=terms_of_service) def _send_recv_regr(self, regr: messages.RegistrationResource, body: messages.Registration) -> messages.RegistrationResource: response = self._post(regr.uri, body) # TODO: Boulder returns httplib.ACCEPTED #assert response.status_code == httplib.OK # TODO: Boulder does not set Location or Link on update # (c.f. acme-spec #94) return self._regr_from_response( response, uri=regr.uri, terms_of_service=regr.terms_of_service) def _post(self, *args: Any, **kwargs: Any) -> requests.Response: """Wrapper around self.net.post that adds the newNonce URL. This is used to retry the request in case of a badNonce error. """ kwargs.setdefault('new_nonce_url', getattr(self.directory, 'newNonce')) return self.net.post(*args, **kwargs) def deactivate_registration(self, regr: messages.RegistrationResource ) -> messages.RegistrationResource: """Deactivate registration. :param messages.RegistrationResource regr: The Registration Resource to be deactivated. :returns: The Registration resource that was deactivated. :rtype: `.RegistrationResource` """ return self.update_registration(regr, messages.Registration.from_json( {"status": "deactivated", "contact": None})) def deactivate_authorization(self, authzr: messages.AuthorizationResource ) -> messages.AuthorizationResource: """Deactivate authorization. :param messages.AuthorizationResource authzr: The Authorization resource to be deactivated. :returns: The Authorization resource that was deactivated. :rtype: `.AuthorizationResource` """ body = messages.UpdateAuthorization(status='deactivated') response = self._post(authzr.uri, body) return self._authzr_from_response(response, authzr.body.identifier, authzr.uri) def _authzr_from_response(self, response: requests.Response, identifier: Optional[messages.Identifier] = None, uri: Optional[str] = None) -> messages.AuthorizationResource: authzr = messages.AuthorizationResource( body=messages.Authorization.from_json(response.json()), uri=response.headers.get('Location', uri)) if identifier is not None and authzr.body.identifier != identifier: # pylint: disable=no-member raise errors.UnexpectedUpdate(authzr) return authzr def answer_challenge(self, challb: messages.ChallengeBody, response: challenges.ChallengeResponse) -> messages.ChallengeResource: """Answer challenge. :param challb: Challenge Resource body. :type challb: `.ChallengeBody` :param response: Corresponding Challenge response :type response: `.challenges.ChallengeResponse` :returns: Challenge Resource with updated body. :rtype: `.ChallengeResource` :raises .UnexpectedUpdate: """ resp = self._post(challb.uri, response) try: authzr_uri = resp.links['up']['url'] except KeyError: raise errors.ClientError('"up" Link header missing') challr = messages.ChallengeResource( authzr_uri=authzr_uri, body=messages.ChallengeBody.from_json(resp.json())) # TODO: check that challr.uri == resp.headers['Location']? if challr.uri != challb.uri: raise errors.UnexpectedUpdate(challr.uri) return challr @classmethod def retry_after(cls, response: requests.Response, default: int) -> datetime.datetime: """Compute next `poll` time based on response ``Retry-After`` header. Handles integers and various datestring formats per https://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.37 :param requests.Response response: Response from `poll`. :param int default: Default value (in seconds), used when ``Retry-After`` header is not present or invalid. :returns: Time point when next `poll` should be performed. :rtype: `datetime.datetime` """ retry_after = response.headers.get('Retry-After', str(default)) try: seconds = int(retry_after) except ValueError: # The RFC 2822 parser handles all of RFC 2616's cases in modern # environments (primarily HTTP 1.1+ but also py27+) when = parsedate_tz(retry_after) if when is not None: try: tz_secs = datetime.timedelta(when[-1] if when[-1] is not None else 0) return datetime.datetime(*when[:7]) - tz_secs except (ValueError, OverflowError): pass seconds = default return datetime.datetime.now() + datetime.timedelta(seconds=seconds) def _revoke(self, cert: x509.Certificate, rsn: int, url: str) -> None: """Revoke certificate. :param .x509.Certificate cert: `x509.Certificate` :param int rsn: Reason code for certificate revocation. :param str url: ACME URL to post to :raises .ClientError: If revocation is unsuccessful. """ response = self._post(url, messages.Revocation( certificate=cert, reason=rsn)) if response.status_code != http_client.OK: raise errors.ClientError( 'Successful revocation must return HTTP OK status') class ClientNetwork: """Wrapper around requests that signs POSTs for authentication. Also adds user agent, and handles Content-Type. """ JSON_CONTENT_TYPE = 'application/json' JOSE_CONTENT_TYPE = 'application/jose+json' JSON_ERROR_CONTENT_TYPE = 'application/problem+json' REPLAY_NONCE_HEADER = 'Replay-Nonce' """Initialize. :param josepy.JWK key: Account private key :param messages.RegistrationResource account: Account object. Required if you are planning to use .post() for anything other than creating a new account; may be set later after registering. :param josepy.JWASignature alg: Algorithm to use in signing JWS. :param bool verify_ssl: Whether to verify certificates on SSL connections. :param str user_agent: String to send as User-Agent header. :param int timeout: Timeout for requests. """ def __init__(self, key: jose.JWK, account: Optional[messages.RegistrationResource] = None, alg: jose.JWASignature = jose.RS256, verify_ssl: bool = True, user_agent: str = 'acme-python', timeout: int = DEFAULT_NETWORK_TIMEOUT) -> None: self.key = key self.account = account self.alg = alg self.verify_ssl = verify_ssl self._nonces: Set[str] = set() self.user_agent = user_agent self.session = requests.Session() self._default_timeout = timeout adapter = HTTPAdapter() self.session.mount("http://", adapter) self.session.mount("https://", adapter) def __del__(self) -> None: # Try to close the session, but don't show exceptions to the # user if the call to close() fails. See #4840. try: self.session.close() except Exception: # pylint: disable=broad-except pass def _wrap_in_jws(self, obj: jose.JSONDeSerializable, nonce: str, url: str) -> str: """Wrap `JSONDeSerializable` object in JWS. .. todo:: Implement ``acmePath``. :param josepy.JSONDeSerializable obj: :param str url: The URL to which this object will be POSTed :param str nonce: :rtype: str """ jobj = obj.json_dumps(indent=2).encode() if obj else b'' logger.debug('JWS payload:\n%s', jobj) kwargs = { "alg": self.alg, "nonce": nonce, "url": url } # newAccount and revokeCert work without the kid # newAccount must not have kid if self.account is not None: kwargs["kid"] = self.account["uri"] kwargs["key"] = self.key return jws.JWS.sign(jobj, **cast(Mapping[str, Any], kwargs)).json_dumps(indent=2) @classmethod def _check_response(cls, response: requests.Response, content_type: Optional[str] = None) -> requests.Response: """Check response content and its type. .. note:: Checking is not strict: wrong server response ``Content-Type`` HTTP header is ignored if response is an expected JSON object (c.f. Boulder #56). :param str content_type: Expected Content-Type response header. If JSON is expected and not present in server response, this function will raise an error. Otherwise, wrong Content-Type is ignored, but logged. :raises .messages.Error: If server response body carries HTTP Problem (https://datatracker.ietf.org/doc/html/rfc7807). :raises .ClientError: In case of other networking errors. """ response_ct = response.headers.get('Content-Type') # Strip parameters from the media-type (rfc2616#section-3.7) if response_ct: response_ct = response_ct.split(';')[0].strip() try: # TODO: response.json() is called twice, once here, and # once in _get and _post clients jobj = response.json() except ValueError: jobj = None if response.status_code == 409: raise errors.ConflictError(response.headers.get('Location', 'UNKNOWN-LOCATION')) if not response.ok: if jobj is not None: if response_ct != cls.JSON_ERROR_CONTENT_TYPE: logger.debug( 'Ignoring wrong Content-Type (%r) for JSON Error', response_ct) try: raise messages.Error.from_json(jobj) except jose.DeserializationError as error: # Couldn't deserialize JSON object raise errors.ClientError((response, error)) else: # response is not JSON object raise errors.ClientError(response) else: if jobj is not None and response_ct != cls.JSON_CONTENT_TYPE: logger.debug( 'Ignoring wrong Content-Type (%r) for JSON decodable ' 'response', response_ct) if content_type == cls.JSON_CONTENT_TYPE and jobj is None: raise errors.ClientError(f'Unexpected response Content-Type: {response_ct}') return response def _send_request(self, method: str, url: str, *args: Any, **kwargs: Any) -> requests.Response: """Send HTTP request. Makes sure that `verify_ssl` is respected. Logs request and response (with headers). For allowed parameters please see `requests.request`. :param str method: method for the new `requests.Request` object :param str url: URL for the new `requests.Request` object :raises requests.exceptions.RequestException: in case of any problems :returns: HTTP Response :rtype: `requests.Response` """ if method == "POST": logger.debug('Sending POST request to %s:\n%s', url, kwargs['data']) else: logger.debug('Sending %s request to %s.', method, url) kwargs['verify'] = self.verify_ssl kwargs.setdefault('headers', {}) kwargs['headers'].setdefault('User-Agent', self.user_agent) kwargs.setdefault('timeout', self._default_timeout) try: response = self.session.request(method, url, *args, **kwargs) except requests.exceptions.RequestException as e: # pylint: disable=pointless-string-statement """Requests response parsing The requests library emits exceptions with a lot of extra text. We parse them with a regexp to raise a more readable exceptions. Example: HTTPSConnectionPool(host='acme-v01.api.letsencrypt.org', port=443): Max retries exceeded with url: /directory (Caused by NewConnectionError(' : Failed to establish a new connection: [Errno 65] No route to host',))""" # pylint: disable=line-too-long err_regex = r".*host='(\S*)'.*Max retries exceeded with url\: (\/\w*).*(\[Errno \d+\])([A-Za-z ]*)" m = re.match(err_regex, str(e)) if m is None: raise # pragma: no cover host, path, _err_no, err_msg = m.groups() raise ValueError(f"Requesting {host}{path}:{err_msg}") # If an Accept header was sent in the request, the response may not be # UTF-8 encoded. In this case, we don't set response.encoding and log # the base64 response instead of raw bytes to keep binary data out of the logs. debug_content: Union[bytes, str] if "Accept" in kwargs["headers"]: debug_content = base64.b64encode(response.content) else: # We set response.encoding so response.text knows the response is # UTF-8 encoded instead of trying to guess the encoding that was # used which is error prone. This setting affects all future # accesses of .text made on the returned response object as well. response.encoding = "utf-8" debug_content = response.text logger.debug('Received response:\nHTTP %d\n%s\n\n%s', response.status_code, "\n".join("{0}: {1}".format(k, v) for k, v in response.headers.items()), debug_content) return response def head(self, *args: Any, **kwargs: Any) -> requests.Response: """Send HEAD request without checking the response. Note, that `_check_response` is not called, as it is expected that status code other than successfully 2xx will be returned, or messages2.Error will be raised by the server. """ return self._send_request('HEAD', *args, **kwargs) def get(self, url: str, content_type: str = JSON_CONTENT_TYPE, **kwargs: Any) -> requests.Response: """Send GET request and check response.""" return self._check_response( self._send_request('GET', url, **kwargs), content_type=content_type) def _add_nonce(self, response: requests.Response) -> None: if self.REPLAY_NONCE_HEADER in response.headers: nonce = response.headers[self.REPLAY_NONCE_HEADER] try: decoded_nonce = jws.Header._fields['nonce'].decode(nonce) except jose.DeserializationError as error: raise errors.BadNonce(nonce, error) logger.debug('Storing nonce: %s', nonce) self._nonces.add(decoded_nonce) else: raise errors.MissingNonce(response) def _get_nonce(self, url: str, new_nonce_url: str) -> str: if not self._nonces: logger.debug('Requesting fresh nonce') if new_nonce_url is None: response = self.head(url) else: # request a new nonce from the acme newNonce endpoint response = self._check_response(self.head(new_nonce_url), content_type=None) self._add_nonce(response) return self._nonces.pop() def post(self, *args: Any, **kwargs: Any) -> requests.Response: """POST object wrapped in `.JWS` and check response. If the server responded with a badNonce error, the request will be retried once. """ try: return self._post_once(*args, **kwargs) except messages.Error as error: if error.code == 'badNonce': logger.debug('Retrying request after error:\n%s', error) return self._post_once(*args, **kwargs) raise def _post_once(self, url: str, obj: jose.JSONDeSerializable, content_type: str = JOSE_CONTENT_TYPE, **kwargs: Any) -> requests.Response: new_nonce_url = kwargs.pop('new_nonce_url', None) data = self._wrap_in_jws(obj, self._get_nonce(url, new_nonce_url), url) kwargs.setdefault('headers', {'Content-Type': content_type}) response = self._send_request('POST', url, data=data, **kwargs) response = self._check_response(response, content_type=content_type) self._add_nonce(response) return response ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1744063413.0 acme-4.0.0/acme/crypto_util.py0000664000175000017500000004354414775045665015371 0ustar00willgwillg"""Crypto utilities.""" import contextlib import enum from datetime import datetime, timedelta, timezone import ipaddress import logging import socket import typing from typing import Any from typing import Callable from typing import List from typing import Literal from typing import Mapping from typing import Optional from typing import Sequence from typing import Set from typing import Tuple from typing import Union from cryptography import x509 from cryptography.hazmat.primitives import hashes, serialization from cryptography.hazmat.primitives.asymmetric import dsa, rsa, ec, ed25519, ed448, types from cryptography.hazmat.primitives.serialization import Encoding from OpenSSL import crypto from OpenSSL import SSL from acme import errors logger = logging.getLogger(__name__) # Default SSL method selected here is the most compatible, while secure # SSL method: TLSv1_METHOD is only compatible with # TLSv1_METHOD, while TLS_method is compatible with all other # methods, including TLSv2_METHOD (read more at # https://docs.openssl.org/master/man3/SSL_CTX_new/#notes). _serve_sni # should be changed to use "set_options" to disable SSLv2 and SSLv3, # in case it's used for things other than probing/serving! _DEFAULT_SSL_METHOD = SSL.TLS_METHOD class Format(enum.IntEnum): """File format to be used when parsing or serializing X.509 structures. Backwards compatible with the `FILETYPE_ASN1` and `FILETYPE_PEM` constants from pyOpenSSL. """ DER = crypto.FILETYPE_ASN1 PEM = crypto.FILETYPE_PEM def to_cryptography_encoding(self) -> Encoding: """Converts the Format to the corresponding cryptography `Encoding`. """ if self == Format.DER: return Encoding.DER else: return Encoding.PEM _KeyAndCert = Union[ Tuple[crypto.PKey, crypto.X509], Tuple[types.CertificateIssuerPrivateKeyTypes, x509.Certificate], ] class _DefaultCertSelection: def __init__(self, certs: Mapping[bytes, _KeyAndCert]): self.certs = certs def __call__(self, connection: SSL.Connection) -> Optional[_KeyAndCert]: server_name = connection.get_servername() if server_name: return self.certs.get(server_name, None) return None # pragma: no cover class SSLSocket: # pylint: disable=too-few-public-methods """SSL wrapper for sockets. :ivar socket sock: Original wrapped socket. :ivar dict certs: Mapping from domain names (`bytes`) to `OpenSSL.crypto.X509`. :ivar method: See `OpenSSL.SSL.Context` for allowed values. :ivar alpn_selection: Hook to select negotiated ALPN protocol for connection. :ivar cert_selection: Hook to select certificate for connection. If given, `certs` parameter would be ignored, and therefore must be empty. """ def __init__( self, sock: socket.socket, certs: Optional[Mapping[bytes, _KeyAndCert]] = None, method: int = _DEFAULT_SSL_METHOD, alpn_selection: Optional[Callable[[SSL.Connection, List[bytes]], bytes]] = None, cert_selection: Optional[ Callable[ [SSL.Connection], Optional[_KeyAndCert], ] ] = None, ) -> None: self.sock = sock self.alpn_selection = alpn_selection self.method = method if not cert_selection and not certs: raise ValueError("Neither cert_selection or certs specified.") if cert_selection and certs: raise ValueError("Both cert_selection and certs specified.") if cert_selection is None: cert_selection = _DefaultCertSelection(certs if certs else {}) self.cert_selection = cert_selection def __getattr__(self, name: str) -> Any: return getattr(self.sock, name) def _pick_certificate_cb(self, connection: SSL.Connection) -> None: """SNI certificate callback. This method will set a new OpenSSL context object for this connection when an incoming connection provides an SNI name (in order to serve the appropriate certificate, if any). :param connection: The TLS connection object on which the SNI extension was received. :type connection: :class:`OpenSSL.Connection` """ pair = self.cert_selection(connection) if pair is None: logger.debug("Certificate selection for server name %s failed, dropping SSL", connection.get_servername()) return key, cert = pair new_context = SSL.Context(self.method) new_context.set_min_proto_version(SSL.TLS1_2_VERSION) new_context.use_privatekey(key) if isinstance(cert, x509.Certificate): cert = crypto.X509.from_cryptography(cert) new_context.use_certificate(cert) if self.alpn_selection is not None: new_context.set_alpn_select_callback(self.alpn_selection) connection.set_context(new_context) class FakeConnection: """Fake OpenSSL.SSL.Connection.""" # pylint: disable=missing-function-docstring def __init__(self, connection: SSL.Connection) -> None: self._wrapped = connection def __getattr__(self, name: str) -> Any: return getattr(self._wrapped, name) def shutdown(self, *unused_args: Any) -> bool: # OpenSSL.SSL.Connection.shutdown doesn't accept any args try: return self._wrapped.shutdown() except SSL.Error as error: # We wrap the error so we raise the same error type as sockets # in the standard library. This is useful when this object is # used by code which expects a standard socket such as # socketserver in the standard library. raise OSError(error) def accept(self) -> Tuple[FakeConnection, Any]: # pylint: disable=missing-function-docstring sock, addr = self.sock.accept() try: context = SSL.Context(self.method) context.set_options(SSL.OP_NO_SSLv2) context.set_options(SSL.OP_NO_SSLv3) context.set_tlsext_servername_callback(self._pick_certificate_cb) if self.alpn_selection is not None: context.set_alpn_select_callback(self.alpn_selection) ssl_sock = self.FakeConnection(SSL.Connection(context, sock)) ssl_sock.set_accept_state() # This log line is especially desirable because without it requests to # our standalone TLSALPN server would not be logged. logger.debug("Performing handshake with %s", addr) try: ssl_sock.do_handshake() except SSL.Error as error: # _pick_certificate_cb might have returned without # creating SSL context (wrong server name) raise OSError(error) return ssl_sock, addr except: # If we encounter any error, close the new socket before reraising # the exception. sock.close() raise def probe_sni(name: bytes, host: bytes, port: int = 443, timeout: int = 300, # pylint: disable=too-many-arguments method: int = _DEFAULT_SSL_METHOD, source_address: Tuple[str, int] = ('', 0), alpn_protocols: Optional[Sequence[bytes]] = None) -> x509.Certificate: """Probe SNI server for SSL certificate. :param bytes name: Byte string to send as the server name in the client hello message. :param bytes host: Host to connect to. :param int port: Port to connect to. :param int timeout: Timeout in seconds. :param method: See `OpenSSL.SSL.Context` for allowed values. :param tuple source_address: Enables multi-path probing (selection of source interface). See `socket.creation_connection` for more info. Available only in Python 2.7+. :param alpn_protocols: Protocols to request using ALPN. :type alpn_protocols: `Sequence` of `bytes` :raises acme.errors.Error: In case of any problems. :returns: SSL certificate presented by the server. :rtype: cryptography.x509.Certificate """ context = SSL.Context(method) context.set_timeout(timeout) socket_kwargs = {'source_address': source_address} try: logger.debug( "Attempting to connect to %s:%d%s.", host, port, " from {0}:{1}".format( source_address[0], source_address[1] ) if any(source_address) else "" ) socket_tuple: Tuple[bytes, int] = (host, port) sock = socket.create_connection(socket_tuple, **socket_kwargs) # type: ignore[arg-type] except OSError as error: raise errors.Error(error) with contextlib.closing(sock) as client: client_ssl = SSL.Connection(context, client) client_ssl.set_connect_state() client_ssl.set_tlsext_host_name(name) # pyOpenSSL>=0.13 if alpn_protocols is not None: client_ssl.set_alpn_protos(list(alpn_protocols)) try: client_ssl.do_handshake() client_ssl.shutdown() except SSL.Error as error: raise errors.Error(error) cert = client_ssl.get_peer_certificate() assert cert # Appease mypy. We would have crashed out by now if there was no certificate. return cert.to_cryptography() # Even *more* annoyingly, due to a mypy bug, we can't use Union[] types in # isinstance expressions without causing false mypy errors. So we have to # recreate the type collection as a tuple here. And no, typing.get_args doesn't # work due to another mypy bug. # # mypy issues: # * https://github.com/python/mypy/issues/17680 # * https://github.com/python/mypy/issues/15106 CertificateIssuerPrivateKeyTypesTpl = ( dsa.DSAPrivateKey, rsa.RSAPrivateKey, ec.EllipticCurvePrivateKey, ed25519.Ed25519PrivateKey, ed448.Ed448PrivateKey, ) def make_csr( private_key_pem: bytes, domains: Optional[Union[Set[str], List[str]]] = None, must_staple: bool = False, ipaddrs: Optional[List[Union[ipaddress.IPv4Address, ipaddress.IPv6Address]]] = None, ) -> bytes: """Generate a CSR containing domains or IPs as subjectAltNames. Parameters are ordered this way for backwards compatibility when called using positional arguments. :param buffer private_key_pem: Private key, in PEM PKCS#8 format. :param list domains: List of DNS names to include in subjectAltNames of CSR. :param bool must_staple: Whether to include the TLS Feature extension (aka OCSP Must Staple: https://tools.ietf.org/html/rfc7633). :param list ipaddrs: List of IPaddress(type ipaddress.IPv4Address or ipaddress.IPv6Address) names to include in subbjectAltNames of CSR. :returns: buffer PEM-encoded Certificate Signing Request. """ private_key = serialization.load_pem_private_key(private_key_pem, password=None) if not isinstance(private_key, CertificateIssuerPrivateKeyTypesTpl): raise ValueError(f"Invalid private key type: {type(private_key)}") if domains is None: domains = [] if ipaddrs is None: ipaddrs = [] if len(domains) + len(ipaddrs) == 0: raise ValueError( "At least one of domains or ipaddrs parameter need to be not empty" ) builder = ( x509.CertificateSigningRequestBuilder() .subject_name(x509.Name([])) .add_extension( x509.SubjectAlternativeName( [x509.DNSName(d) for d in domains] + [x509.IPAddress(i) for i in ipaddrs] ), critical=False, ) ) if must_staple: builder = builder.add_extension( # "status_request" is the feature commonly known as OCSP # Must-Staple x509.TLSFeature([x509.TLSFeatureType.status_request]), critical=False, ) csr = builder.sign(private_key, hashes.SHA256()) return csr.public_bytes(Encoding.PEM) def get_names_from_subject_and_extensions( subject: x509.Name, exts: x509.Extensions ) -> List[str]: """Gets all DNS SAN names as well as the first Common Name from subject. :param subject: Name of the x509 object, which may include Common Name :type subject: `cryptography.x509.Name` :param exts: Extensions of the x509 object, which may include SANs :type exts: `cryptography.x509.Extensions` :returns: List of DNS Subject Alternative Names and first Common Name :rtype: `list` of `str` """ # We know these are always `str` because `bytes` is only possible for # other OIDs. cns = [ typing.cast(str, c.value) for c in subject.get_attributes_for_oid(x509.NameOID.COMMON_NAME) ] try: san_ext = exts.get_extension_for_class(x509.SubjectAlternativeName) except x509.ExtensionNotFound: dns_names = [] else: dns_names = san_ext.value.get_values_for_type(x509.DNSName) if not cns: return dns_names else: # We only include the first CN, if there are multiple. This matches # the behavior of the previously implementation using pyOpenSSL. return [cns[0]] + [d for d in dns_names if d != cns[0]] def _cryptography_cert_or_req_san( cert_or_req: Union[x509.Certificate, x509.CertificateSigningRequest], ) -> List[str]: """Get Subject Alternative Names from certificate or CSR using pyOpenSSL. .. note:: Although this is `acme` internal API, it is used by `letsencrypt`. :param cert_or_req: Certificate or CSR. :type cert_or_req: `x509.Certificate` or `x509.CertificateSigningRequest`. :returns: A list of Subject Alternative Names that is DNS. :rtype: `list` of `str` Deprecated .. deprecated: 3.2.1 """ # ???: is this translation needed? exts = cert_or_req.extensions try: san_ext = exts.get_extension_for_class(x509.SubjectAlternativeName) except x509.ExtensionNotFound: return [] return san_ext.value.get_values_for_type(x509.DNSName) # Helper function that can be mocked in unit tests def _now() -> datetime: return datetime.now(tz=timezone.utc) def make_self_signed_cert(private_key: types.CertificateIssuerPrivateKeyTypes, domains: Optional[List[str]] = None, not_before: Optional[datetime] = None, validity: Optional[timedelta] = None, force_san: bool = True, extensions: Optional[List[x509.Extension]] = None, ips: Optional[List[Union[ipaddress.IPv4Address, ipaddress.IPv6Address]]] = None ) -> x509.Certificate: """Generate new self-signed certificate. :param buffer private_key_pem: Private key, in PEM PKCS#8 format. :type domains: `list` of `str` :param int not_before: A datetime after which the cert is valid. If no timezone is specified, UTC is assumed :type not_before: `datetime.datetime` :param validity: Duration for which the cert will be valid. Defaults to 1 week :type validity: `datetime.timedelta` :param buffer private_key_pem: One of `cryptography.hazmat.primitives.asymmetric.types.CertificateIssuerPrivateKeyTypes` :param bool force_san: :param extensions: List of additional extensions to include in the cert. :type extensions: `list` of `x509.Extension[x509.ExtensionType]` :type ips: `list` of (`ipaddress.IPv4Address` or `ipaddress.IPv6Address`) If more than one domain is provided, all of the domains are put into ``subjectAltName`` X.509 extension and first domain is set as the subject CN. If only one domain is provided no ``subjectAltName`` extension is used, unless `force_san` is ``True``. """ assert domains or ips, "Must provide one or more hostnames or IPs for the cert." builder = x509.CertificateBuilder() builder = builder.serial_number(x509.random_serial_number()) if extensions is not None: for ext in extensions: builder = builder.add_extension(ext.value, ext.critical) if domains is None: domains = [] if ips is None: ips = [] builder = builder.add_extension(x509.BasicConstraints(ca=True, path_length=0), critical=True) name_attrs = [] if len(domains) > 0: name_attrs.append(x509.NameAttribute( x509.OID_COMMON_NAME, domains[0] )) builder = builder.subject_name(x509.Name(name_attrs)) builder = builder.issuer_name(x509.Name(name_attrs)) sanlist: List[x509.GeneralName] = [] for address in domains: sanlist.append(x509.DNSName(address)) for ip in ips: sanlist.append(x509.IPAddress(ip)) if force_san or len(domains) > 1 or len(ips) > 0: builder = builder.add_extension( x509.SubjectAlternativeName(sanlist), critical=False ) if not_before is None: not_before = _now() if validity is None: validity = timedelta(seconds=7 * 24 * 60 * 60) builder = builder.not_valid_before(not_before) builder = builder.not_valid_after(not_before + validity) public_key = private_key.public_key() builder = builder.public_key(public_key) return builder.sign(private_key, hashes.SHA256()) def dump_cryptography_chain( chain: List[x509.Certificate], encoding: Literal[Encoding.PEM, Encoding.DER] = Encoding.PEM, ) -> bytes: """Dump certificate chain into a bundle. :param list chain: List of `cryptography.x509.Certificate`. :returns: certificate chain bundle :rtype: bytes Deprecated .. deprecated: 3.2.1 """ # XXX: returns empty string when no chain is available, which # shuts up RenewableCert, but might not be the best solution... def _dump_cert(cert: x509.Certificate) -> bytes: return cert.public_bytes(encoding) # assumes that x509.Certificate.public_bytes includes ending # newline character return b"".join(_dump_cert(cert) for cert in chain) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1744063413.0 acme-4.0.0/acme/errors.py0000664000175000017500000001033314775045665014316 0ustar00willgwillg"""ACME errors.""" import typing from typing import Any from typing import List from typing import Mapping from typing import Set from josepy import errors as jose_errors import requests # We import acme.messages only during type check to avoid circular dependencies. Type references # to acme.message.* must be quoted to be lazily initialized and avoid compilation errors. if typing.TYPE_CHECKING: from acme import messages # pragma: no cover class Error(Exception): """Generic ACME error.""" class DependencyError(Error): """Dependency error""" class SchemaValidationError(jose_errors.DeserializationError): """JSON schema ACME object validation error.""" class ClientError(Error): """Network error.""" class UnexpectedUpdate(ClientError): """Unexpected update error.""" class NonceError(ClientError): """Server response nonce error.""" class BadNonce(NonceError): """Bad nonce error.""" def __init__(self, nonce: str, error: Exception, *args: Any) -> None: super().__init__(*args) self.nonce = nonce self.error = error def __str__(self) -> str: return 'Invalid nonce ({0!r}): {1}'.format(self.nonce, self.error) class MissingNonce(NonceError): """Missing nonce error. According to the specification an "ACME server MUST include an Replay-Nonce header field in each successful response to a POST it provides to a client (...)". :ivar requests.Response ~.response: HTTP Response """ def __init__(self, response: requests.Response, *args: Any) -> None: super().__init__(*args) self.response = response def __str__(self) -> str: return ('Server {0} response did not include a replay ' 'nonce, headers: {1} (This may be a service outage)'.format( self.response.request.method, self.response.headers)) class PollError(ClientError): """Generic error when polling for authorization fails. This might be caused by either timeout (`exhausted` will be non-empty) or by some authorization being invalid. :ivar exhausted: Set of `.AuthorizationResource` that didn't finish within max allowed attempts. :ivar updated: Mapping from original `.AuthorizationResource` to the most recently updated one """ def __init__(self, exhausted: Set['messages.AuthorizationResource'], updated: Mapping['messages.AuthorizationResource', 'messages.AuthorizationResource'] ) -> None: self.exhausted = exhausted self.updated = updated super().__init__() @property def timeout(self) -> bool: """Was the error caused by timeout?""" return bool(self.exhausted) def __repr__(self) -> str: return '{0}(exhausted={1!r}, updated={2!r})'.format( self.__class__.__name__, self.exhausted, self.updated) class ValidationError(Error): """Error for authorization failures. Contains a list of authorization resources, each of which is invalid and should have an error field. """ def __init__(self, failed_authzrs: List['messages.AuthorizationResource']) -> None: self.failed_authzrs = failed_authzrs super().__init__() class TimeoutError(Error): # pylint: disable=redefined-builtin """Error for when polling an authorization or an order times out.""" class IssuanceError(Error): """Error sent by the server after requesting issuance of a certificate.""" def __init__(self, error: 'messages.Error') -> None: """Initialize. :param messages.Error error: The error provided by the server. """ self.error = error super().__init__() class ConflictError(ClientError): """Error for when the server returns a 409 (Conflict) HTTP status. In the version of ACME implemented by Boulder, this is used to find an account if you only have the private key, but don't know the account URL. Also used in V2 of the ACME client for the same purpose. """ def __init__(self, location: str) -> None: self.location = location super().__init__() class WildcardUnsupportedError(Error): """Error for when a wildcard is requested but is unsupported by ACME CA.""" ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1744063413.0 acme-4.0.0/acme/fields.py0000664000175000017500000000316414775045665014254 0ustar00willgwillg"""ACME JSON fields.""" import datetime import logging from typing import Any import josepy as jose import pyrfc3339 logger = logging.getLogger(__name__) class Fixed(jose.Field): """Fixed field.""" def __init__(self, json_name: str, value: Any) -> None: self.value = value super().__init__( json_name=json_name, default=value, omitempty=False) def decode(self, value: Any) -> Any: if value != self.value: raise jose.DeserializationError('Expected {0!r}'.format(self.value)) return self.value def encode(self, value: Any) -> Any: if value != self.value: logger.warning( 'Overriding fixed field (%s) with %r', self.json_name, value) return value class RFC3339Field(jose.Field): """RFC3339 field encoder/decoder. Handles decoding/encoding between RFC3339 strings and aware (not naive) `datetime.datetime` objects (e.g. ``datetime.datetime.now(pytz.UTC)``). """ @classmethod def default_encoder(cls, value: datetime.datetime) -> str: return pyrfc3339.generate(value) @classmethod def default_decoder(cls, value: str) -> datetime.datetime: try: return pyrfc3339.parse(value) except ValueError as error: raise jose.DeserializationError(error) def fixed(json_name: str, value: Any) -> Any: """Generates a type-friendly Fixed field.""" return Fixed(json_name, value) def rfc3339(json_name: str, omitempty: bool = False) -> Any: """Generates a type-friendly RFC3339 field.""" return RFC3339Field(json_name, omitempty=omitempty) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1744063413.0 acme-4.0.0/acme/jws.py0000664000175000017500000000500414775045665013604 0ustar00willgwillg"""ACME-specific JWS. The JWS implementation in josepy only implements the base JOSE standard. In order to support the new header fields defined in ACME, this module defines some ACME-specific classes that layer on top of josepy. """ from typing import Optional import josepy as jose class Header(jose.Header): """ACME-specific JOSE Header. Implements nonce, kid, and url. """ nonce: Optional[bytes] = jose.field('nonce', omitempty=True, encoder=jose.encode_b64jose) kid: Optional[str] = jose.field('kid', omitempty=True) url: Optional[str] = jose.field('url', omitempty=True) # Mypy does not understand the josepy magic happening here, and falsely claims # that nonce is redefined. Let's ignore the type check here. @nonce.decoder # type: ignore[no-redef,union-attr] def nonce(value: str) -> bytes: # type: ignore[misc] # pylint: disable=no-self-argument,missing-function-docstring try: return jose.decode_b64jose(value) except jose.DeserializationError as error: # TODO: custom error raise jose.DeserializationError("Invalid nonce: {0}".format(error)) class Signature(jose.Signature): """ACME-specific Signature. Uses ACME-specific Header for customer fields.""" __slots__ = jose.Signature._orig_slots # pylint: disable=protected-access,no-member # TODO: decoder/encoder should accept cls? Otherwise, subclassing # JSONObjectWithFields is tricky... header_cls = Header header: Header = jose.field( 'header', omitempty=True, default=header_cls(), decoder=header_cls.from_json) # TODO: decoder should check that nonce is in the protected header class JWS(jose.JWS): """ACME-specific JWS. Includes none, url, and kid in protected header.""" signature_cls = Signature __slots__ = jose.JWS._orig_slots # pylint: disable=protected-access @classmethod # type: ignore[override] # pylint: disable=arguments-differ def sign(cls, payload: bytes, key: jose.JWK, alg: jose.JWASignature, nonce: Optional[bytes], url: Optional[str] = None, kid: Optional[str] = None) -> jose.JWS: # Per ACME spec, jwk and kid are mutually exclusive, so only include a # jwk field if kid is not provided. include_jwk = kid is None return super().sign(payload, key=key, alg=alg, protect=frozenset(['nonce', 'url', 'kid', 'jwk', 'alg']), nonce=nonce, url=url, kid=kid, include_jwk=include_jwk) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1744063413.0 acme-4.0.0/acme/messages.py0000664000175000017500000006351114775045665014617 0ustar00willgwillg"""ACME protocol messages.""" from collections.abc import Hashable import datetime import json from typing import Any from typing import Dict from typing import Iterator from typing import List from typing import Mapping from typing import MutableMapping from typing import Optional from typing import Tuple from typing import Type from typing import TypeVar from cryptography import x509 import josepy as jose from acme import challenges from acme import errors from acme import fields from acme import jws from acme import util ERROR_PREFIX = "urn:ietf:params:acme:error:" ERROR_CODES = { 'accountDoesNotExist': 'The request specified an account that does not exist', 'alreadyRevoked': 'The request specified a certificate to be revoked that has' \ ' already been revoked', 'badCSR': 'The CSR is unacceptable (e.g., due to a short key)', 'badNonce': 'The client sent an unacceptable anti-replay nonce', 'badPublicKey': 'The JWS was signed by a public key the server does not support', 'badRevocationReason': 'The revocation reason provided is not allowed by the server', 'badSignatureAlgorithm': 'The JWS was signed with an algorithm the server does not support', 'caa': 'Certification Authority Authorization (CAA) records forbid the CA from issuing' \ ' a certificate', 'compound': 'Specific error conditions are indicated in the "subproblems" array', 'connection': ('The server could not connect to the client to verify the' ' domain'), 'dns': 'There was a problem with a DNS query during identifier validation', 'dnssec': 'The server could not validate a DNSSEC signed domain', 'incorrectResponse': 'Response received didn\'t match the challenge\'s requirements', # deprecate invalidEmail 'invalidEmail': 'The provided email for a registration was invalid', 'invalidContact': 'The provided contact URI was invalid', 'malformed': 'The request message was malformed', 'rejectedIdentifier': 'The server will not issue certificates for the identifier', 'orderNotReady': 'The request attempted to finalize an order that is not ready to be finalized', 'rateLimited': 'There were too many requests of a given type', 'serverInternal': 'The server experienced an internal error', 'tls': 'The server experienced a TLS error during domain verification', 'unauthorized': 'The client lacks sufficient authorization', 'unsupportedContact': 'A contact URL for an account used an unsupported protocol scheme', 'unknownHost': 'The server could not resolve a domain name', 'unsupportedIdentifier': 'An identifier is of an unsupported type', 'externalAccountRequired': 'The server requires external account binding', } ERROR_TYPE_DESCRIPTIONS = {**{ ERROR_PREFIX + name: desc for name, desc in ERROR_CODES.items() }} def is_acme_error(err: BaseException) -> bool: """Check if argument is an ACME error.""" if isinstance(err, Error) and (err.typ is not None): return ERROR_PREFIX in err.typ return False class _Constant(jose.JSONDeSerializable, Hashable): """ACME constant.""" __slots__ = ('name',) POSSIBLE_NAMES: Dict[str, '_Constant'] = NotImplemented def __init__(self, name: str) -> None: super().__init__() self.POSSIBLE_NAMES[name] = self # pylint: disable=unsupported-assignment-operation self.name = name def to_partial_json(self) -> str: return self.name @classmethod def from_json(cls, jobj: str) -> '_Constant': if jobj not in cls.POSSIBLE_NAMES: # pylint: disable=unsupported-membership-test raise jose.DeserializationError(f'{cls.__name__} not recognized') return cls.POSSIBLE_NAMES[jobj] def __repr__(self) -> str: return f'{self.__class__.__name__}({self.name})' def __eq__(self, other: Any) -> bool: return isinstance(other, type(self)) and other.name == self.name def __hash__(self) -> int: return hash((self.__class__, self.name)) class IdentifierType(_Constant): """ACME identifier type.""" POSSIBLE_NAMES: Dict[str, _Constant] = {} IDENTIFIER_FQDN = IdentifierType('dns') # IdentifierDNS in Boulder IDENTIFIER_IP = IdentifierType('ip') # IdentifierIP in pebble - not in Boulder yet class Identifier(jose.JSONObjectWithFields): """ACME identifier. :ivar IdentifierType typ: :ivar str value: """ typ: IdentifierType = jose.field('type', decoder=IdentifierType.from_json) value: str = jose.field('value') class Error(jose.JSONObjectWithFields, errors.Error): """ACME error. https://datatracker.ietf.org/doc/html/rfc7807 Note: Although Error inherits from JSONObjectWithFields, which is immutable, we add mutability for Error to comply with the Python exception API. :ivar str typ: :ivar str title: :ivar str detail: :ivar Identifier identifier: :ivar tuple subproblems: An array of ACME Errors which may be present when the CA returns multiple errors related to the same request, `tuple` of `Error`. """ typ: str = jose.field('type', omitempty=True, default='about:blank') title: str = jose.field('title', omitempty=True) detail: str = jose.field('detail', omitempty=True) identifier: Optional['Identifier'] = jose.field( 'identifier', decoder=Identifier.from_json, omitempty=True) subproblems: Optional[Tuple['Error', ...]] = jose.field('subproblems', omitempty=True) # Mypy does not understand the josepy magic happening here, and falsely claims # that subproblems is redefined. Let's ignore the type check here. @subproblems.decoder # type: ignore def subproblems(value: List[Dict[str, Any]]) -> Tuple['Error', ...]: # pylint: disable=no-self-argument,missing-function-docstring return tuple(Error.from_json(subproblem) for subproblem in value) @classmethod def with_code(cls, code: str, **kwargs: Any) -> 'Error': """Create an Error instance with an ACME Error code. :str code: An ACME error code, like 'dnssec'. :kwargs: kwargs to pass to Error. """ if code not in ERROR_CODES: raise ValueError("The supplied code: %s is not a known ACME error" " code" % code) typ = ERROR_PREFIX + code # Mypy will not understand that the Error constructor accepts a named argument # "typ" because of josepy magic. Let's ignore the type check here. return cls(typ=typ, **kwargs) @property def description(self) -> Optional[str]: """Hardcoded error description based on its type. :returns: Description if standard ACME error or ``None``. :rtype: str """ return ERROR_TYPE_DESCRIPTIONS.get(self.typ) @property def code(self) -> Optional[str]: """ACME error code. Basically self.typ without the ERROR_PREFIX. :returns: error code if standard ACME code or ``None``. :rtype: str """ code = str(self.typ).rsplit(':', maxsplit=1)[-1] if code in ERROR_CODES: return code return None # Hack to allow mutability on Errors (see GH #9539) def __setattr__(self, name: str, value: Any) -> None: return object.__setattr__(self, name, value) def __str__(self) -> str: result = b' :: '.join( part.encode('ascii', 'backslashreplace') for part in (self.typ, self.description, self.detail, self.title) if part is not None).decode() if self.identifier: result = f'Problem for {self.identifier.value}: ' + result # pylint: disable=no-member if self.subproblems and len(self.subproblems) > 0: for subproblem in self.subproblems: result += f'\n{subproblem}' return result class Status(_Constant): """ACME "status" field.""" POSSIBLE_NAMES: Dict[str, _Constant] = {} STATUS_UNKNOWN = Status('unknown') STATUS_PENDING = Status('pending') STATUS_PROCESSING = Status('processing') STATUS_VALID = Status('valid') STATUS_INVALID = Status('invalid') STATUS_REVOKED = Status('revoked') STATUS_READY = Status('ready') STATUS_DEACTIVATED = Status('deactivated') class Directory(jose.JSONDeSerializable): """Directory. Directory resources must be accessed by the exact field name in RFC8555 (section 9.7.5). """ class Meta(jose.JSONObjectWithFields): """Directory Meta.""" _terms_of_service: str = jose.field('termsOfService', omitempty=True) website: str = jose.field('website', omitempty=True) caa_identities: List[str] = jose.field('caaIdentities', omitempty=True) external_account_required: bool = jose.field('externalAccountRequired', omitempty=True) profiles: Dict[str, str] = jose.field('profiles', omitempty=True) def __init__(self, **kwargs: Any) -> None: kwargs = {self._internal_name(k): v for k, v in kwargs.items()} super().__init__(**kwargs) @property def terms_of_service(self) -> str: """URL for the CA TOS""" return self._terms_of_service def __iter__(self) -> Iterator[str]: # When iterating over fields, use the external name 'terms_of_service' instead of # the internal '_terms_of_service'. for name in super().__iter__(): yield name[1:] if name == '_terms_of_service' else name def _internal_name(self, name: str) -> str: return '_' + name if name == 'terms_of_service' else name def __init__(self, jobj: Mapping[str, Any]) -> None: self._jobj = jobj def __getattr__(self, name: str) -> Any: try: return self[name] except KeyError as error: raise AttributeError(str(error)) def __getitem__(self, name: str) -> Any: try: return self._jobj[name] except KeyError: raise KeyError(f'Directory field "{name}" not found') def to_partial_json(self) -> Dict[str, Any]: return util.map_keys(self._jobj, lambda k: k) @classmethod def from_json(cls, jobj: MutableMapping[str, Any]) -> 'Directory': jobj['meta'] = cls.Meta.from_json(jobj.pop('meta', {})) return cls(jobj) class Resource(jose.JSONObjectWithFields): """ACME Resource. :ivar acme.messages.ResourceBody body: Resource body. """ body: "ResourceBody" = jose.field('body') class ResourceWithURI(Resource): """ACME Resource with URI. :ivar str uri: Location of the resource. """ uri: str = jose.field('uri') # no ChallengeResource.uri class ResourceBody(jose.JSONObjectWithFields): """ACME Resource Body.""" class ExternalAccountBinding: """ACME External Account Binding""" @classmethod def from_data(cls, account_public_key: jose.JWK, kid: str, hmac_key: str, directory: Directory) -> Dict[str, Any]: """Create External Account Binding Resource from contact details, kid and hmac.""" key_json = json.dumps(account_public_key.to_partial_json()).encode() decoded_hmac_key = jose.b64.b64decode(hmac_key) url = directory["newAccount"] eab = jws.JWS.sign(key_json, jose.jwk.JWKOct(key=decoded_hmac_key), jose.jwa.HS256, None, url, kid) return eab.to_partial_json() GenericRegistration = TypeVar('GenericRegistration', bound='Registration') class Registration(ResourceBody): """Registration Resource Body. :ivar jose.JWK key: Public key. :ivar tuple contact: Contact information following ACME spec, `tuple` of `str`. :ivar str agreement: """ # on new-reg key server ignores 'key' and populates it based on # JWS.signature.combined.jwk key: jose.JWK = jose.field('key', omitempty=True, decoder=jose.JWK.from_json) # Contact field implements special behavior to allow messages that clear existing # contacts while not expecting the `contact` field when loading from json. # This is implemented in the constructor and *_json methods. contact: Tuple[str, ...] = jose.field('contact', omitempty=True, default=()) agreement: str = jose.field('agreement', omitempty=True) status: Status = jose.field('status', omitempty=True) terms_of_service_agreed: bool = jose.field('termsOfServiceAgreed', omitempty=True) only_return_existing: bool = jose.field('onlyReturnExisting', omitempty=True) external_account_binding: Dict[str, Any] = jose.field('externalAccountBinding', omitempty=True) phone_prefix = 'tel:' email_prefix = 'mailto:' @classmethod def from_data(cls: Type[GenericRegistration], phone: Optional[str] = None, email: Optional[str] = None, external_account_binding: Optional[Dict[str, Any]] = None, **kwargs: Any) -> GenericRegistration: """ Create registration resource from contact details. The `contact` keyword being passed to a Registration object is meaningful, so this function represents empty iterables in its kwargs by passing on an empty `tuple`. """ # Note if `contact` was in kwargs. contact_provided = 'contact' in kwargs # Pop `contact` from kwargs and add formatted email or phone numbers details = list(kwargs.pop('contact', ())) if phone is not None: details.append(cls.phone_prefix + phone) if email is not None: details.extend([cls.email_prefix + mail for mail in email.split(',')]) # Insert formatted contact information back into kwargs # or insert an empty tuple if `contact` provided. if details or contact_provided: kwargs['contact'] = tuple(details) if external_account_binding: kwargs['external_account_binding'] = external_account_binding return cls(**kwargs) def __init__(self, **kwargs: Any) -> None: """Note if the user provides a value for the `contact` member.""" if 'contact' in kwargs and kwargs['contact'] is not None: # Avoid the __setattr__ used by jose.TypedJSONObjectWithFields object.__setattr__(self, '_add_contact', True) super().__init__(**kwargs) def _filter_contact(self, prefix: str) -> Tuple[str, ...]: return tuple( detail[len(prefix):] for detail in self.contact # pylint: disable=not-an-iterable if detail.startswith(prefix)) def _add_contact_if_appropriate(self, jobj: Dict[str, Any]) -> Dict[str, Any]: """ The `contact` member of Registration objects should not be required when de-serializing (as it would be if the Fields' `omitempty` flag were `False`), but it should be included in serializations if it was provided. :param jobj: Dictionary containing this Registrations' data :type jobj: dict :returns: Dictionary containing Registrations data to transmit to the server :rtype: dict """ if getattr(self, '_add_contact', False): jobj['contact'] = self.encode('contact') return jobj def to_partial_json(self) -> Dict[str, Any]: """Modify josepy.JSONDeserializable.to_partial_json()""" jobj = super().to_partial_json() return self._add_contact_if_appropriate(jobj) def fields_to_partial_json(self) -> Dict[str, Any]: """Modify josepy.JSONObjectWithFields.fields_to_partial_json()""" jobj = super().fields_to_partial_json() return self._add_contact_if_appropriate(jobj) @property def phones(self) -> Tuple[str, ...]: """All phones found in the ``contact`` field.""" return self._filter_contact(self.phone_prefix) @property def emails(self) -> Tuple[str, ...]: """All emails found in the ``contact`` field.""" return self._filter_contact(self.email_prefix) class NewRegistration(Registration): """New registration.""" class UpdateRegistration(Registration): """Update registration.""" class RegistrationResource(ResourceWithURI): """Registration Resource. :ivar acme.messages.Registration body: :ivar str new_authzr_uri: Deprecated. Do not use. :ivar str terms_of_service: URL for the CA TOS. """ body: Registration = jose.field('body', decoder=Registration.from_json) new_authzr_uri: str = jose.field('new_authzr_uri', omitempty=True) terms_of_service: str = jose.field('terms_of_service', omitempty=True) class ChallengeBody(ResourceBody): """Challenge Resource Body. .. todo:: Confusingly, this has a similar name to `.challenges.Challenge`, as well as `.achallenges.AnnotatedChallenge`. Please use names such as ``challb`` to distinguish instances of this class from ``achall``. :ivar acme.challenges.Challenge: Wrapped challenge. Conveniently, all challenge fields are proxied, i.e. you can call ``challb.x`` to get ``challb.chall.x`` contents. :ivar acme.messages.Status status: :ivar datetime.datetime validated: :ivar messages.Error error: """ __slots__ = ('chall',) # ACMEv1 has a "uri" field in challenges. ACMEv2 has a "url" field. This # challenge object supports either one, but should be accessed through the # name "uri". In Client.answer_challenge, whichever one is set will be # used. _url: str = jose.field('url', omitempty=True, default=None) status: Status = jose.field('status', decoder=Status.from_json, omitempty=True, default=STATUS_PENDING) validated: datetime.datetime = fields.rfc3339('validated', omitempty=True) error: Error = jose.field('error', decoder=Error.from_json, omitempty=True, default=None) def __init__(self, **kwargs: Any) -> None: kwargs = {self._internal_name(k): v for k, v in kwargs.items()} super().__init__(**kwargs) def encode(self, name: str) -> Any: return super().encode(self._internal_name(name)) def to_partial_json(self) -> Dict[str, Any]: jobj = super().to_partial_json() jobj.update(self.chall.to_partial_json()) return jobj @classmethod def fields_from_json(cls, jobj: Mapping[str, Any]) -> Dict[str, Any]: jobj_fields = super().fields_from_json(jobj) jobj_fields['chall'] = challenges.Challenge.from_json(jobj) return jobj_fields @property def uri(self) -> str: """The URL of this challenge.""" return self._url def __getattr__(self, name: str) -> Any: return getattr(self.chall, name) def __iter__(self) -> Iterator[str]: # When iterating over fields, use the external name 'uri' instead of # the internal '_uri'. for name in super().__iter__(): yield 'uri' if name == '_url' else name def _internal_name(self, name: str) -> str: return '_url' if name == 'uri' else name class ChallengeResource(Resource): """Challenge Resource. :ivar acme.messages.ChallengeBody body: :ivar str authzr_uri: URI found in the 'up' ``Link`` header. """ body: ChallengeBody = jose.field('body', decoder=ChallengeBody.from_json) authzr_uri: str = jose.field('authzr_uri') @property def uri(self) -> str: """The URL of the challenge body.""" return self.body.uri # pylint: disable=no-member class Authorization(ResourceBody): """Authorization Resource Body. :ivar acme.messages.Identifier identifier: :ivar list challenges: `list` of `.ChallengeBody` :ivar acme.messages.Status status: :ivar datetime.datetime expires: """ identifier: Identifier = jose.field('identifier', decoder=Identifier.from_json, omitempty=True) challenges: List[ChallengeBody] = jose.field('challenges', omitempty=True) status: Status = jose.field('status', omitempty=True, decoder=Status.from_json) # TODO: 'expires' is allowed for Authorization Resources in # general, but for Key Authorization '[t]he "expires" field MUST # be absent'... then acme-spec gives example with 'expires' # present... That's confusing! expires: datetime.datetime = fields.rfc3339('expires', omitempty=True) wildcard: bool = jose.field('wildcard', omitempty=True) # Mypy does not understand the josepy magic happening here, and falsely claims # that challenge is redefined. Let's ignore the type check here. @challenges.decoder # type: ignore def challenges(value: List[Dict[str, Any]]) -> Tuple[ChallengeBody, ...]: # pylint: disable=no-self-argument,missing-function-docstring return tuple(ChallengeBody.from_json(chall) for chall in value) class NewAuthorization(Authorization): """New authorization.""" class UpdateAuthorization(Authorization): """Update authorization.""" class AuthorizationResource(ResourceWithURI): """Authorization Resource. :ivar acme.messages.Authorization body: :ivar str new_cert_uri: Deprecated. Do not use. """ body: Authorization = jose.field('body', decoder=Authorization.from_json) new_cert_uri: str = jose.field('new_cert_uri', omitempty=True) class CertificateRequest(jose.JSONObjectWithFields): """ACME newOrder request. :ivar x509.CertificateSigningRequest csr: `x509.CertificateSigningRequest` """ csr: x509.CertificateSigningRequest = jose.field( 'csr', decoder=jose.decode_csr, encoder=jose.encode_csr) class CertificateResource(ResourceWithURI): """Certificate Resource. :ivar x509.Certificate body: `x509.Certificate` :ivar str cert_chain_uri: URI found in the 'up' ``Link`` header :ivar tuple authzrs: `tuple` of `AuthorizationResource`. """ cert_chain_uri: str = jose.field('cert_chain_uri') authzrs: Tuple[AuthorizationResource, ...] = jose.field('authzrs') class Revocation(jose.JSONObjectWithFields): """Revocation message. :ivar x509.Certificate certificate: `x509.Certificate` """ certificate: x509.Certificate = jose.field( 'certificate', decoder=jose.decode_cert, encoder=jose.encode_cert) reason: int = jose.field('reason') class Order(ResourceBody): """Order Resource Body. :ivar profile: The profile to request. :vartype profile: str :ivar identifiers: List of identifiers for the certificate. :vartype identifiers: `list` of `.Identifier` :ivar acme.messages.Status status: :ivar authorizations: URLs of authorizations. :vartype authorizations: `list` of `str` :ivar str certificate: URL to download certificate as a fullchain PEM. :ivar str finalize: URL to POST to to request issuance once all authorizations have "valid" status. :ivar datetime.datetime expires: When the order expires. :ivar ~.Error error: Any error that occurred during finalization, if applicable. """ # https://datatracker.ietf.org/doc/draft-aaron-acme-profiles/ profile: str = jose.field('profile', omitempty=True) identifiers: List[Identifier] = jose.field('identifiers', omitempty=True) status: Status = jose.field('status', decoder=Status.from_json, omitempty=True) authorizations: List[str] = jose.field('authorizations', omitempty=True) certificate: str = jose.field('certificate', omitempty=True) finalize: str = jose.field('finalize', omitempty=True) expires: datetime.datetime = fields.rfc3339('expires', omitempty=True) error: Error = jose.field('error', omitempty=True, decoder=Error.from_json) # Mypy does not understand the josepy magic happening here, and falsely claims # that identifiers is redefined. Let's ignore the type check here. @identifiers.decoder # type: ignore def identifiers(value: List[Dict[str, Any]]) -> Tuple[Identifier, ...]: # pylint: disable=no-self-argument,missing-function-docstring return tuple(Identifier.from_json(identifier) for identifier in value) class OrderResource(ResourceWithURI): """Order Resource. :ivar acme.messages.Order body: :ivar bytes csr_pem: The CSR this Order will be finalized with. :ivar authorizations: Fully-fetched AuthorizationResource objects. :vartype authorizations: `list` of `acme.messages.AuthorizationResource` :ivar str fullchain_pem: The fetched contents of the certificate URL produced once the order was finalized, if it's present. :ivar alternative_fullchains_pem: The fetched contents of alternative certificate chain URLs produced once the order was finalized, if present and requested during finalization. :vartype alternative_fullchains_pem: `list` of `str` """ body: Order = jose.field('body', decoder=Order.from_json) csr_pem: bytes = jose.field('csr_pem', omitempty=True, # This looks backwards, but it's not - # we want the deserialized value to be # `bytes`, but anything we put into # JSON needs to be `str`, so we encode # to decode and decode to # encode. Otherwise we end up with an # array of ints on serialization decoder=lambda s: s.encode("utf-8"), encoder=lambda b: b.decode("utf-8")) authorizations: List[AuthorizationResource] = jose.field('authorizations') fullchain_pem: str = jose.field('fullchain_pem', omitempty=True) alternative_fullchains_pem: List[str] = jose.field('alternative_fullchains_pem', omitempty=True) # Mypy does not understand the josepy magic happening here, and falsely claims # that authorizations is redefined. Let's ignore the type check here. @authorizations.decoder # type: ignore def authorizations(value: List[Dict[str, Any]]) -> Tuple[AuthorizationResource, ...]: # pylint: disable=no-self-argument,missing-function-docstring return tuple(AuthorizationResource.from_json(authz) for authz in value) class NewOrder(Order): """New order.""" ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1744063413.0 acme-4.0.0/acme/py.typed0000664000175000017500000000000014775045665014115 0ustar00willgwillg././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1744063413.0 acme-4.0.0/acme/standalone.py0000664000175000017500000003104614775045665015136 0ustar00willgwillg"""Support for standalone client challenge solvers. """ import collections import functools import http.client as http_client import http.server as BaseHTTPServer import logging import socket import socketserver import threading from typing import Any from typing import cast from typing import List from typing import Mapping from typing import Optional from typing import Set from typing import Tuple from typing import Type from OpenSSL import SSL from acme import challenges from acme import crypto_util logger = logging.getLogger(__name__) class TLSServer(socketserver.TCPServer): """Generic TLS Server.""" def __init__(self, *args: Any, **kwargs: Any) -> None: self.ipv6 = kwargs.pop("ipv6", False) if self.ipv6: self.address_family = socket.AF_INET6 else: self.address_family = socket.AF_INET self.certs = kwargs.pop("certs", {}) self.method = kwargs.pop("method", crypto_util._DEFAULT_SSL_METHOD) self.allow_reuse_address = kwargs.pop("allow_reuse_address", True) super().__init__(*args, **kwargs) def _wrap_sock(self) -> None: self.socket = cast(socket.socket, crypto_util.SSLSocket( self.socket, cert_selection=self._cert_selection, alpn_selection=getattr(self, '_alpn_selection', None), method=self.method)) def _cert_selection(self, connection: SSL.Connection ) -> Optional[crypto_util._KeyAndCert]: # pragma: no cover """Callback selecting certificate for connection.""" server_name = connection.get_servername() if server_name: return self.certs.get(server_name, None) return None def server_bind(self) -> None: self._wrap_sock() return socketserver.TCPServer.server_bind(self) class ACMEServerMixin: """ACME server common settings mixin.""" # TODO: c.f. #858 server_version = "ACME client standalone challenge solver" allow_reuse_address = True class BaseDualNetworkedServers: """Base class for a pair of IPv6 and IPv4 servers that tries to do everything it's asked for both servers, but where failures in one server don't affect the other. If two servers are instantiated, they will serve on the same port. """ def __init__(self, ServerClass: Type[socketserver.TCPServer], server_address: Tuple[str, int], *remaining_args: Any, **kwargs: Any) -> None: port = server_address[1] self.threads: List[threading.Thread] = [] self.servers: List[socketserver.BaseServer] = [] # Preserve socket error for re-raising, if no servers can be started last_socket_err: Optional[socket.error] = None # Must try True first. # Ubuntu, for example, will fail to bind to IPv4 if we've already bound # to IPv6. But that's ok, since it will accept IPv4 connections on the IPv6 # socket. On the other hand, FreeBSD will successfully bind to IPv4 on the # same port, which means that server will accept the IPv4 connections. # If Python is compiled without IPv6, we'll error out but (probably) successfully # create the IPv4 server. for ip_version in [True, False]: try: kwargs["ipv6"] = ip_version new_address = (server_address[0],) + (port,) + server_address[2:] new_args = (new_address,) + remaining_args server = ServerClass(*new_args, **kwargs) logger.debug( "Successfully bound to %s:%s using %s", new_address[0], new_address[1], "IPv6" if ip_version else "IPv4") except OSError as e: last_socket_err = e if self.servers: # Already bound using IPv6. logger.debug( "Certbot wasn't able to bind to %s:%s using %s, this " "is often expected due to the dual stack nature of " "IPv6 socket implementations.", new_address[0], new_address[1], "IPv6" if ip_version else "IPv4") else: logger.debug( "Failed to bind to %s:%s using %s", new_address[0], new_address[1], "IPv6" if ip_version else "IPv4") else: self.servers.append(server) # If two servers are set up and port 0 was passed in, ensure we always # bind to the same port for both servers. port = server.socket.getsockname()[1] if not self.servers: if last_socket_err: raise last_socket_err else: # pragma: no cover raise OSError("Could not bind to IPv4 or IPv6.") def serve_forever(self) -> None: """Wraps socketserver.TCPServer.serve_forever""" for server in self.servers: thread = threading.Thread( target=server.serve_forever) thread.start() self.threads.append(thread) def getsocknames(self) -> List[Tuple[str, int]]: """Wraps socketserver.TCPServer.socket.getsockname""" return [server.socket.getsockname() for server in self.servers] def shutdown_and_server_close(self) -> None: """Wraps socketserver.TCPServer.shutdown, socketserver.TCPServer.server_close, and threading.Thread.join""" for server in self.servers: server.shutdown() server.server_close() for thread in self.threads: thread.join() self.threads = [] class TLSALPN01Server(TLSServer, ACMEServerMixin): """TLSALPN01 Server.""" ACME_TLS_1_PROTOCOL = b"acme-tls/1" def __init__(self, server_address: Tuple[str, int], certs: List[crypto_util._KeyAndCert], challenge_certs: Mapping[bytes, crypto_util._KeyAndCert], ipv6: bool = False) -> None: # We don't need to implement a request handler here because the work # (including logging) is being done by wrapped socket set up in the # parent TLSServer class. TLSServer.__init__( self, server_address, socketserver.BaseRequestHandler, certs=certs, ipv6=ipv6) self.challenge_certs = challenge_certs def _cert_selection(self, connection: SSL.Connection) -> Optional[crypto_util._KeyAndCert]: # TODO: We would like to serve challenge cert only if asked for it via # ALPN. To do this, we need to retrieve the list of protos from client # hello, but this is currently impossible with openssl [0], and ALPN # negotiation is done after cert selection. # Therefore, currently we always return challenge cert, and terminate # handshake in alpn_selection() if ALPN protos are not what we expect. # [0] https://github.com/openssl/openssl/issues/4952 server_name = connection.get_servername() if server_name: logger.debug("Serving challenge cert for server name %s", server_name) return self.challenge_certs[server_name] return None # pragma: no cover def _alpn_selection(self, _connection: SSL.Connection, alpn_protos: List[bytes]) -> bytes: """Callback to select alpn protocol.""" if len(alpn_protos) == 1 and alpn_protos[0] == self.ACME_TLS_1_PROTOCOL: logger.debug("Agreed on %s ALPN", self.ACME_TLS_1_PROTOCOL) return self.ACME_TLS_1_PROTOCOL logger.debug("Cannot agree on ALPN proto. Got: %s", str(alpn_protos)) # Explicitly close the connection now, by returning an empty string. # See https://www.pyopenssl.org/en/stable/api/ssl.html#OpenSSL.SSL.Context.set_alpn_select_callback # pylint: disable=line-too-long return b"" class HTTPServer(BaseHTTPServer.HTTPServer): """Generic HTTP Server.""" def __init__(self, *args: Any, **kwargs: Any) -> None: self.ipv6 = kwargs.pop("ipv6", False) if self.ipv6: self.address_family = socket.AF_INET6 else: self.address_family = socket.AF_INET super().__init__(*args, **kwargs) class HTTP01Server(HTTPServer, ACMEServerMixin): """HTTP01 Server.""" def __init__(self, server_address: Tuple[str, int], resources: Set[challenges.HTTP01], ipv6: bool = False, timeout: int = 30) -> None: super().__init__( server_address, HTTP01RequestHandler.partial_init( simple_http_resources=resources, timeout=timeout), ipv6=ipv6) class HTTP01DualNetworkedServers(BaseDualNetworkedServers): """HTTP01Server Wrapper. Tries everything for both. Failures for one don't affect the other.""" def __init__(self, *args: Any, **kwargs: Any) -> None: super().__init__(HTTP01Server, *args, **kwargs) class HTTP01RequestHandler(BaseHTTPServer.BaseHTTPRequestHandler): """HTTP01 challenge handler. Adheres to the stdlib's `socketserver.BaseRequestHandler` interface. :ivar set simple_http_resources: A set of `HTTP01Resource` objects. TODO: better name? """ HTTP01Resource = collections.namedtuple( "HTTP01Resource", "chall response validation") def __init__(self, *args: Any, **kwargs: Any) -> None: self.simple_http_resources = kwargs.pop("simple_http_resources", set()) self._timeout = kwargs.pop('timeout', 30) super().__init__(*args, **kwargs) self.server: HTTP01Server # In parent class BaseHTTPRequestHandler, 'timeout' is a class-level property but we # need to define its value during the initialization phase in HTTP01RequestHandler. # However MyPy does not appreciate that we dynamically shadow a class-level property # with an instance-level property (eg. self.timeout = ... in __init__()). So to make # everyone happy, we statically redefine 'timeout' as a method property, and set the # timeout value in a new internal instance-level property _timeout. @property def timeout(self) -> int: # type: ignore[override] """ The default timeout this server should apply to requests. :return: timeout to apply :rtype: int """ return self._timeout def log_message(self, format: str, *args: Any) -> None: # pylint: disable=redefined-builtin """Log arbitrary message.""" logger.debug("%s - - %s", self.client_address[0], format % args) def handle(self) -> None: """Handle request.""" self.log_message("Incoming request") BaseHTTPServer.BaseHTTPRequestHandler.handle(self) def do_GET(self) -> None: # pylint: disable=invalid-name,missing-function-docstring if self.path == "/": self.handle_index() elif self.path.startswith("/" + challenges.HTTP01.URI_ROOT_PATH): self.handle_simple_http_resource() else: self.handle_404() def handle_index(self) -> None: """Handle index page.""" self.send_response(200) self.send_header("Content-Type", "text/html") self.end_headers() self.wfile.write(self.server.server_version.encode()) def handle_404(self) -> None: """Handler 404 Not Found errors.""" self.send_response(http_client.NOT_FOUND, message="Not Found") self.send_header("Content-type", "text/html") self.end_headers() self.wfile.write(b"404") def handle_simple_http_resource(self) -> None: """Handle HTTP01 provisioned resources.""" for resource in self.simple_http_resources: if resource.chall.path == self.path: self.log_message("Serving HTTP01 with token %r", resource.chall.encode("token")) self.send_response(http_client.OK) self.end_headers() self.wfile.write(resource.validation.encode()) return else: # pylint: disable=useless-else-on-loop self.log_message("No resources to serve") self.log_message("%s does not correspond to any resource. ignoring", self.path) @classmethod def partial_init(cls, simple_http_resources: Set[challenges.HTTP01], timeout: int) -> 'functools.partial[HTTP01RequestHandler]': """Partially initialize this handler. This is useful because `socketserver.BaseServer` takes uninitialized handler and initializes it with the current request. """ return functools.partial( cls, simple_http_resources=simple_http_resources, timeout=timeout) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1744063413.0 acme-4.0.0/acme/util.py0000664000175000017500000000045714775045665013765 0ustar00willgwillg"""ACME utilities.""" from typing import Any from typing import Callable from typing import Dict from typing import Mapping def map_keys(dikt: Mapping[Any, Any], func: Callable[[Any], Any]) -> Dict[Any, Any]: """Map dictionary keys.""" return {func(key): value for key, value in dikt.items()} ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1744063414.9735498 acme-4.0.0/acme.egg-info/0000775000175000017500000000000014775045667014124 5ustar00willgwillg././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1744063414.0 acme-4.0.0/acme.egg-info/PKG-INFO0000644000175000017500000000275214775045666015224 0ustar00willgwillgMetadata-Version: 2.4 Name: acme Version: 4.0.0 Summary: ACME protocol implementation in Python Home-page: https://github.com/certbot/certbot Author: Certbot Project Author-email: certbot-dev@eff.org License: Apache License 2.0 Classifier: Development Status :: 5 - Production/Stable Classifier: Intended Audience :: Developers Classifier: License :: OSI Approved :: Apache Software License Classifier: Programming Language :: Python Classifier: Programming Language :: Python :: 3 Classifier: Programming Language :: Python :: 3.9 Classifier: Programming Language :: Python :: 3.10 Classifier: Programming Language :: Python :: 3.11 Classifier: Programming Language :: Python :: 3.12 Classifier: Programming Language :: Python :: 3.13 Classifier: Topic :: Internet :: WWW/HTTP Classifier: Topic :: Security Requires-Python: >=3.9 License-File: LICENSE.txt Requires-Dist: cryptography>=43.0.0 Requires-Dist: josepy>=2.0.0 Requires-Dist: PyOpenSSL>=25.0.0 Requires-Dist: pyrfc3339 Requires-Dist: pytz>=2019.3 Requires-Dist: requests>=2.20.0 Provides-Extra: docs Requires-Dist: Sphinx>=1.0; extra == "docs" Requires-Dist: sphinx_rtd_theme; extra == "docs" Provides-Extra: test Requires-Dist: pytest; extra == "test" Requires-Dist: pytest-xdist; extra == "test" Requires-Dist: typing-extensions; extra == "test" Dynamic: author Dynamic: author-email Dynamic: classifier Dynamic: home-page Dynamic: license Dynamic: license-file Dynamic: provides-extra Dynamic: requires-dist Dynamic: requires-python Dynamic: summary ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1744063414.0 acme-4.0.0/acme.egg-info/SOURCES.txt0000664000175000017500000000501614775045666016011 0ustar00willgwillgLICENSE.txt MANIFEST.in README.rst pytest.ini setup.py acme/__init__.py acme/challenges.py acme/client.py acme/crypto_util.py acme/errors.py acme/fields.py acme/jws.py acme/messages.py acme/py.typed acme/standalone.py acme/util.py acme.egg-info/PKG-INFO acme.egg-info/SOURCES.txt acme.egg-info/dependency_links.txt acme.egg-info/requires.txt acme.egg-info/top_level.txt acme/_internal/__init__.py acme/_internal/tests/__init__.py acme/_internal/tests/challenges_test.py acme/_internal/tests/client_test.py acme/_internal/tests/crypto_util_test.py acme/_internal/tests/errors_test.py acme/_internal/tests/fields_test.py acme/_internal/tests/jose_test.py acme/_internal/tests/jws_test.py acme/_internal/tests/messages_test.py acme/_internal/tests/standalone_test.py acme/_internal/tests/test_util.py acme/_internal/tests/util_test.py acme/_internal/tests/testdata/README acme/_internal/tests/testdata/cert-100sans.pem acme/_internal/tests/testdata/cert-idnsans.pem acme/_internal/tests/testdata/cert-ipsans.pem acme/_internal/tests/testdata/cert-ipv6sans.pem acme/_internal/tests/testdata/cert-nocn.der acme/_internal/tests/testdata/cert-san.pem acme/_internal/tests/testdata/cert.der acme/_internal/tests/testdata/cert.pem acme/_internal/tests/testdata/critical-san.pem acme/_internal/tests/testdata/csr-100sans.pem acme/_internal/tests/testdata/csr-6sans.pem acme/_internal/tests/testdata/csr-idnsans.pem acme/_internal/tests/testdata/csr-ipsans.pem acme/_internal/tests/testdata/csr-ipv6sans.pem acme/_internal/tests/testdata/csr-mixed.pem acme/_internal/tests/testdata/csr-nosans.pem acme/_internal/tests/testdata/csr-san.pem acme/_internal/tests/testdata/csr.der acme/_internal/tests/testdata/csr.pem acme/_internal/tests/testdata/dsa512_key.pem acme/_internal/tests/testdata/ec_secp384r1_key.pem acme/_internal/tests/testdata/rsa1024_cert.pem acme/_internal/tests/testdata/rsa1024_key.pem acme/_internal/tests/testdata/rsa2048_cert.pem acme/_internal/tests/testdata/rsa2048_key.pem acme/_internal/tests/testdata/rsa256_key.pem acme/_internal/tests/testdata/rsa4096_cert.pem acme/_internal/tests/testdata/rsa4096_key.pem acme/_internal/tests/testdata/rsa512_key.pem docs/.gitignore docs/Makefile docs/api.rst docs/conf.py docs/index.rst docs/jws-help.txt docs/make.bat docs/_static/.gitignore docs/_templates/.gitignore docs/api/challenges.rst docs/api/client.rst docs/api/crypto_util.rst docs/api/errors.rst docs/api/fields.rst docs/api/jose.rst docs/api/jws.rst docs/api/messages.rst docs/api/standalone.rst docs/api/util.rst docs/man/jws.rst examples/http01_example.py././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1744063414.0 acme-4.0.0/acme.egg-info/dependency_links.txt0000664000175000017500000000000114775045666020171 0ustar00willgwillg ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1744063414.0 acme-4.0.0/acme.egg-info/requires.txt0000664000175000017500000000026014775045666016521 0ustar00willgwillgcryptography>=43.0.0 josepy>=2.0.0 PyOpenSSL>=25.0.0 pyrfc3339 pytz>=2019.3 requests>=2.20.0 [docs] Sphinx>=1.0 sphinx_rtd_theme [test] pytest pytest-xdist typing-extensions ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1744063414.0 acme-4.0.0/acme.egg-info/top_level.txt0000664000175000017500000000000514775045666016650 0ustar00willgwillgacme ././@PaxHeader0000000000000000000000000000003200000000000010210 xustar0026 mtime=1744063414.97255 acme-4.0.0/docs/0000775000175000017500000000000014775045667012455 5ustar00willgwillg././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1744063413.0 acme-4.0.0/docs/.gitignore0000664000175000017500000000001114775045665014433 0ustar00willgwillg/_build/ ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1744063413.0 acme-4.0.0/docs/Makefile0000664000175000017500000001641214775045665014117 0ustar00willgwillg# Makefile for Sphinx documentation # # You can set these variables from the command line. SPHINXOPTS = SPHINXBUILD = sphinx-build PAPER = BUILDDIR = _build # User-friendly check for sphinx-build ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1) $(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don't have Sphinx installed, grab it from https://www.sphinx-doc.org/) endif # Internal variables. PAPEROPT_a4 = -D latex_paper_size=a4 PAPEROPT_letter = -D latex_paper_size=letter ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . # the i18n builder cannot share the environment and doctrees with the others I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest coverage gettext help: @echo "Please use \`make ' where is one of" @echo " html to make standalone HTML files" @echo " dirhtml to make HTML files named index.html in directories" @echo " singlehtml to make a single large HTML file" @echo " pickle to make pickle files" @echo " json to make JSON files" @echo " htmlhelp to make HTML files and a HTML help project" @echo " qthelp to make HTML files and a qthelp project" @echo " applehelp to make an Apple Help Book" @echo " devhelp to make HTML files and a Devhelp project" @echo " epub to make an epub" @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" @echo " latexpdf to make LaTeX files and run them through pdflatex" @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx" @echo " text to make text files" @echo " man to make manual pages" @echo " texinfo to make Texinfo files" @echo " info to make Texinfo files and run them through makeinfo" @echo " gettext to make PO message catalogs" @echo " changes to make an overview of all changed/added/deprecated items" @echo " xml to make Docutils-native XML files" @echo " pseudoxml to make pseudoxml-XML files for display purposes" @echo " linkcheck to check all external links for integrity" @echo " doctest to run all doctests embedded in the documentation (if enabled)" @echo " coverage to run coverage check of the documentation (if enabled)" clean: rm -rf $(BUILDDIR)/* html: $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." dirhtml: $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." singlehtml: $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml @echo @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." pickle: $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle @echo @echo "Build finished; now you can process the pickle files." json: $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json @echo @echo "Build finished; now you can process the JSON files." htmlhelp: $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp @echo @echo "Build finished; now you can run HTML Help Workshop with the" \ ".hhp project file in $(BUILDDIR)/htmlhelp." qthelp: $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp @echo @echo "Build finished; now you can run "qcollectiongenerator" with the" \ ".qhcp project file in $(BUILDDIR)/qthelp, like this:" @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/acme-python.qhcp" @echo "To view the help file:" @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/acme-python.qhc" applehelp: $(SPHINXBUILD) -b applehelp $(ALLSPHINXOPTS) $(BUILDDIR)/applehelp @echo @echo "Build finished. The help book is in $(BUILDDIR)/applehelp." @echo "N.B. You won't be able to view it unless you put it in" \ "~/Library/Documentation/Help or install it in your application" \ "bundle." devhelp: $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp @echo @echo "Build finished." @echo "To view the help file:" @echo "# mkdir -p $$HOME/.local/share/devhelp/acme-python" @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/acme-python" @echo "# devhelp" epub: $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub @echo @echo "Build finished. The epub file is in $(BUILDDIR)/epub." latex: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." @echo "Run \`make' in that directory to run these through (pdf)latex" \ "(use \`make latexpdf' here to do that automatically)." latexpdf: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo "Running LaTeX files through pdflatex..." $(MAKE) -C $(BUILDDIR)/latex all-pdf @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." latexpdfja: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo "Running LaTeX files through platex and dvipdfmx..." $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." text: $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text @echo @echo "Build finished. The text files are in $(BUILDDIR)/text." man: $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man @echo @echo "Build finished. The manual pages are in $(BUILDDIR)/man." texinfo: $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo @echo @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." @echo "Run \`make' in that directory to run these through makeinfo" \ "(use \`make info' here to do that automatically)." info: $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo @echo "Running Texinfo files through makeinfo..." make -C $(BUILDDIR)/texinfo info @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." gettext: $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale @echo @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." changes: $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes @echo @echo "The overview file is in $(BUILDDIR)/changes." linkcheck: $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck @echo @echo "Link check complete; look for any errors in the above output " \ "or in $(BUILDDIR)/linkcheck/output.txt." doctest: $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest @echo "Testing of doctests in the sources finished, look at the " \ "results in $(BUILDDIR)/doctest/output.txt." coverage: $(SPHINXBUILD) -b coverage $(ALLSPHINXOPTS) $(BUILDDIR)/coverage @echo "Testing of coverage in the sources finished, look at the " \ "results in $(BUILDDIR)/coverage/python.txt." xml: $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml @echo @echo "Build finished. The XML files are in $(BUILDDIR)/xml." pseudoxml: $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml @echo @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml." ././@PaxHeader0000000000000000000000000000003200000000000010210 xustar0026 mtime=1744063414.97255 acme-4.0.0/docs/_static/0000775000175000017500000000000014775045667014103 5ustar00willgwillg././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1744063413.0 acme-4.0.0/docs/_static/.gitignore0000664000175000017500000000000014775045665016057 0ustar00willgwillg././@PaxHeader0000000000000000000000000000003200000000000010210 xustar0026 mtime=1744063414.97255 acme-4.0.0/docs/_templates/0000775000175000017500000000000014775045667014612 5ustar00willgwillg././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1744063413.0 acme-4.0.0/docs/_templates/.gitignore0000664000175000017500000000000014775045665016566 0ustar00willgwillg././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1744063414.9735498 acme-4.0.0/docs/api/0000775000175000017500000000000014775045667013226 5ustar00willgwillg././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1744063413.0 acme-4.0.0/docs/api/challenges.rst0000664000175000017500000000010414775045665016056 0ustar00willgwillgChallenges ---------- .. automodule:: acme.challenges :members: ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1744063413.0 acme-4.0.0/docs/api/client.rst0000664000175000017500000000007014775045665015231 0ustar00willgwillgClient ------ .. automodule:: acme.client :members: ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1744063413.0 acme-4.0.0/docs/api/crypto_util.rst0000664000175000017500000000010714775045665016331 0ustar00willgwillgCrypto_util ----------- .. automodule:: acme.crypto_util :members: ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1744063413.0 acme-4.0.0/docs/api/errors.rst0000664000175000017500000000007014775045665015267 0ustar00willgwillgErrors ------ .. automodule:: acme.errors :members: ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1744063413.0 acme-4.0.0/docs/api/fields.rst0000664000175000017500000000007014775045665015221 0ustar00willgwillgFields ------ .. automodule:: acme.fields :members: ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1744063413.0 acme-4.0.0/docs/api/jose.rst0000664000175000017500000000024114775045665014713 0ustar00willgwillgJOSE ---- The ``acme.jose`` module was moved to its own package "josepy_". Please refer to its documentation there. .. _josepy: https://josepy.readthedocs.io/ ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1744063413.0 acme-4.0.0/docs/api/jws.rst0000664000175000017500000000005714775045665014563 0ustar00willgwillgJWS --- .. automodule:: acme.jws :members: ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1744063413.0 acme-4.0.0/docs/api/messages.rst0000664000175000017500000000007614775045665015570 0ustar00willgwillgMessages -------- .. automodule:: acme.messages :members: ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1744063413.0 acme-4.0.0/docs/api/standalone.rst0000664000175000017500000000010414775045665016101 0ustar00willgwillgStandalone ---------- .. automodule:: acme.standalone :members: ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1744063413.0 acme-4.0.0/docs/api/util.rst0000664000175000017500000000006214775045665014731 0ustar00willgwillgUtil ---- .. automodule:: acme.util :members: ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1744063413.0 acme-4.0.0/docs/api.rst0000664000175000017500000000013014775045665013750 0ustar00willgwillg================= API Documentation ================= .. toctree:: :glob: api/* ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1744063413.0 acme-4.0.0/docs/conf.py0000664000175000017500000002310314775045665013751 0ustar00willgwillg# -*- coding: utf-8 -*- # # acme-python documentation build configuration file, created by # sphinx-quickstart on Sun Oct 18 13:38:06 2015. # # This file is execfile()d with the current directory set to its # containing dir. # # Note that not all possible configuration values are present in this # autogenerated file. # # All configuration values have a default; values that are commented out # serve to show the default. import os import sys here = os.path.abspath(os.path.dirname(__file__)) # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. sys.path.insert(0, os.path.abspath(os.path.join(here, '..'))) # -- General configuration ------------------------------------------------ # If your documentation needs a minimal Sphinx version, state it here. needs_sphinx = '1.0' # Add any Sphinx extension module names here, as strings. They can be # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom # ones. extensions = [ 'sphinx.ext.autodoc', 'sphinx.ext.intersphinx', 'sphinx.ext.todo', 'sphinx.ext.coverage', 'sphinx.ext.viewcode', 'sphinx_rtd_theme', ] autodoc_member_order = 'bysource' autodoc_default_flags = ['show-inheritance'] # Add any paths that contain templates here, relative to this directory. templates_path = ['_templates'] # The suffix(es) of source filenames. # You can specify multiple suffix as a list of string: # source_suffix = ['.rst', '.md'] source_suffix = '.rst' # The encoding of source files. #source_encoding = 'utf-8-sig' # The master toctree document. master_doc = 'index' # General information about the project. project = u'acme-python' copyright = u'2015, Let\'s Encrypt Project' author = u'Let\'s Encrypt Project' # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the # built documents. # # The short X.Y version. version = '0' # The full version, including alpha/beta/rc tags. release = '0' # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. # # This is also used if you do content translation via gettext catalogs. # Usually you set "language" from the command line for these cases. language = 'en' # There are two options for replacing |today|: either, you set today to some # non-false value, then it is used: #today = '' # Else, today_fmt is used as the format for a strftime call. #today_fmt = '%B %d, %Y' # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. exclude_patterns = [ '_build', ] # The reST default role (used for this markup: `text`) to use for all # documents. default_role = 'py:obj' # If true, '()' will be appended to :func: etc. cross-reference text. #add_function_parentheses = True # If true, the current module name will be prepended to all description # unit titles (such as .. function::). #add_module_names = True # If true, sectionauthor and moduleauthor directives will be shown in the # output. They are ignored by default. #show_authors = False # The name of the Pygments (syntax highlighting) style to use. pygments_style = 'sphinx' # A list of ignored prefixes for module index sorting. #modindex_common_prefix = [] # If true, keep warnings as "system message" paragraphs in the built documents. #keep_warnings = False # If true, `todo` and `todoList` produce output, else they produce nothing. todo_include_todos = False # -- Options for HTML output ---------------------------------------------- # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. html_theme = 'sphinx_rtd_theme' # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the # documentation. #html_theme_options = {} # Add any paths that contain custom themes here, relative to this directory. #html_theme_path = [] # The name for this set of Sphinx documents. If None, it defaults to # " v documentation". #html_title = None # A shorter title for the navigation bar. Default is the same as html_title. #html_short_title = None # The name of an image file (relative to this directory) to place at the top # of the sidebar. #html_logo = None # The name of an image file (within the static path) to use as favicon of the # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 # pixels large. #html_favicon = None # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". html_static_path = ['_static'] # Add any extra paths that contain custom files (such as robots.txt or # .htaccess) here, relative to this directory. These files are copied # directly to the root of the documentation. #html_extra_path = [] # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, # using the given strftime format. #html_last_updated_fmt = '%b %d, %Y' # If true, SmartyPants will be used to convert quotes and dashes to # typographically correct entities. #html_use_smartypants = True # Custom sidebar templates, maps document names to template names. #html_sidebars = {} # Additional templates that should be rendered to pages, maps page names to # template names. #html_additional_pages = {} # If false, no module index is generated. #html_domain_indices = True # If false, no index is generated. #html_use_index = True # If true, the index is split into individual pages for each letter. #html_split_index = False # If true, links to the reST sources are added to the pages. #html_show_sourcelink = True # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. #html_show_sphinx = True # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. #html_show_copyright = True # If true, an OpenSearch description file will be output, and all pages will # contain a tag referring to it. The value of this option must be the # base URL from which the finished HTML is served. #html_use_opensearch = '' # This is the file name suffix for HTML files (e.g. ".xhtml"). #html_file_suffix = None # Language to be used for generating the HTML full-text search index. # Sphinx supports the following languages: # 'da', 'de', 'en', 'es', 'fi', 'fr', 'hu', 'it', 'ja' # 'nl', 'no', 'pt', 'ro', 'ru', 'sv', 'tr' #html_search_language = 'en' # A dictionary with options for the search language support, empty by default. # Now only 'ja' uses this config value #html_search_options = {'type': 'default'} # The name of a javascript file (relative to the configuration directory) that # implements a search results scorer. If empty, the default will be used. #html_search_scorer = 'scorer.js' # Output file base name for HTML help builder. htmlhelp_basename = 'acme-pythondoc' # -- Options for LaTeX output --------------------------------------------- latex_elements = { # The paper size ('letterpaper' or 'a4paper'). #'papersize': 'letterpaper', # The font size ('10pt', '11pt' or '12pt'). #'pointsize': '10pt', # Additional stuff for the LaTeX preamble. #'preamble': '', # Latex figure (float) alignment #'figure_align': 'htbp', } # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, # author, documentclass [howto, manual, or own class]). latex_documents = [ (master_doc, 'acme-python.tex', u'acme-python Documentation', u'Let\'s Encrypt Project', 'manual'), ] # The name of an image file (relative to this directory) to place at the top of # the title page. #latex_logo = None # For "manual" documents, if this is true, then toplevel headings are parts, # not chapters. #latex_use_parts = False # If true, show page references after internal links. #latex_show_pagerefs = False # If true, show URL addresses after external links. #latex_show_urls = False # Documents to append as an appendix to all manuals. #latex_appendices = [] # If false, no module index is generated. #latex_domain_indices = True # -- Options for manual page output --------------------------------------- # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). man_pages = [ (master_doc, 'acme-python', u'acme-python Documentation', [author], 1), ('man/jws', 'jws', u'jws script documentation', [project], 1), ] # If true, show URL addresses after external links. #man_show_urls = False # -- Options for Texinfo output ------------------------------------------- # Grouping the document tree into Texinfo files. List of tuples # (source start file, target name, title, author, # dir menu entry, description, category) texinfo_documents = [ (master_doc, 'acme-python', u'acme-python Documentation', author, 'acme-python', 'One line description of project.', 'Miscellaneous'), ] # Documents to append as an appendix to all manuals. #texinfo_appendices = [] # If false, no module index is generated. #texinfo_domain_indices = True # How to display URL addresses: 'footnote', 'no', or 'inline'. #texinfo_show_urls = 'footnote' # If true, do not generate a @detailmenu in the "Top" node's menu. #texinfo_no_detailmenu = False intersphinx_mapping = { 'python': ('https://docs.python.org/', None), 'josepy': ('https://josepy.readthedocs.io/en/latest/', None), } ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1744063413.0 acme-4.0.0/docs/index.rst0000664000175000017500000000073714775045665014323 0ustar00willgwillg.. acme-python documentation master file, created by sphinx-quickstart on Sun Oct 18 13:38:06 2015. You can adapt this file completely to your liking, but it should at least contain the root `toctree` directive. Welcome to acme-python's documentation! ======================================= Contents: .. toctree:: :maxdepth: 2 api .. automodule:: acme :members: Indices and tables ================== * :ref:`genindex` * :ref:`modindex` * :ref:`search` ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1744063413.0 acme-4.0.0/docs/jws-help.txt0000664000175000017500000000024714775045665014750 0ustar00willgwillgusage: jws [-h] [--compact] {sign,verify} ... positional arguments: {sign,verify} optional arguments: -h, --help show this help message and exit --compact ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1744063413.0 acme-4.0.0/docs/make.bat0000664000175000017500000001613314775045665014064 0ustar00willgwillg@ECHO OFF REM Command file for Sphinx documentation if "%SPHINXBUILD%" == "" ( set SPHINXBUILD=sphinx-build ) set BUILDDIR=_build set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% . set I18NSPHINXOPTS=%SPHINXOPTS% . if NOT "%PAPER%" == "" ( set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS% set I18NSPHINXOPTS=-D latex_paper_size=%PAPER% %I18NSPHINXOPTS% ) if "%1" == "" goto help if "%1" == "help" ( :help echo.Please use `make ^` where ^ is one of echo. html to make standalone HTML files echo. dirhtml to make HTML files named index.html in directories echo. singlehtml to make a single large HTML file echo. pickle to make pickle files echo. json to make JSON files echo. htmlhelp to make HTML files and a HTML help project echo. qthelp to make HTML files and a qthelp project echo. devhelp to make HTML files and a Devhelp project echo. epub to make an epub echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter echo. text to make text files echo. man to make manual pages echo. texinfo to make Texinfo files echo. gettext to make PO message catalogs echo. changes to make an overview over all changed/added/deprecated items echo. xml to make Docutils-native XML files echo. pseudoxml to make pseudoxml-XML files for display purposes echo. linkcheck to check all external links for integrity echo. doctest to run all doctests embedded in the documentation if enabled echo. coverage to run coverage check of the documentation if enabled goto end ) if "%1" == "clean" ( for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i del /q /s %BUILDDIR%\* goto end ) REM Check if sphinx-build is available and fallback to Python version if any %SPHINXBUILD% 2> nul if errorlevel 9009 goto sphinx_python goto sphinx_ok :sphinx_python set SPHINXBUILD=python -m sphinx.__init__ %SPHINXBUILD% 2> nul if errorlevel 9009 ( echo. echo.The 'sphinx-build' command was not found. Make sure you have Sphinx echo.installed, then set the SPHINXBUILD environment variable to point echo.to the full path of the 'sphinx-build' executable. Alternatively you echo.may add the Sphinx directory to PATH. echo. echo.If you don't have Sphinx installed, grab it from echo.https://www.sphinx-doc.org/ exit /b 1 ) :sphinx_ok if "%1" == "html" ( %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html if errorlevel 1 exit /b 1 echo. echo.Build finished. The HTML pages are in %BUILDDIR%/html. goto end ) if "%1" == "dirhtml" ( %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml if errorlevel 1 exit /b 1 echo. echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml. goto end ) if "%1" == "singlehtml" ( %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml if errorlevel 1 exit /b 1 echo. echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml. goto end ) if "%1" == "pickle" ( %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle if errorlevel 1 exit /b 1 echo. echo.Build finished; now you can process the pickle files. goto end ) if "%1" == "json" ( %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json if errorlevel 1 exit /b 1 echo. echo.Build finished; now you can process the JSON files. goto end ) if "%1" == "htmlhelp" ( %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp if errorlevel 1 exit /b 1 echo. echo.Build finished; now you can run HTML Help Workshop with the ^ .hhp project file in %BUILDDIR%/htmlhelp. goto end ) if "%1" == "qthelp" ( %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp if errorlevel 1 exit /b 1 echo. echo.Build finished; now you can run "qcollectiongenerator" with the ^ .qhcp project file in %BUILDDIR%/qthelp, like this: echo.^> qcollectiongenerator %BUILDDIR%\qthelp\acme-python.qhcp echo.To view the help file: echo.^> assistant -collectionFile %BUILDDIR%\qthelp\acme-python.ghc goto end ) if "%1" == "devhelp" ( %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp if errorlevel 1 exit /b 1 echo. echo.Build finished. goto end ) if "%1" == "epub" ( %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub if errorlevel 1 exit /b 1 echo. echo.Build finished. The epub file is in %BUILDDIR%/epub. goto end ) if "%1" == "latex" ( %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex if errorlevel 1 exit /b 1 echo. echo.Build finished; the LaTeX files are in %BUILDDIR%/latex. goto end ) if "%1" == "latexpdf" ( %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex cd %BUILDDIR%/latex make all-pdf cd %~dp0 echo. echo.Build finished; the PDF files are in %BUILDDIR%/latex. goto end ) if "%1" == "latexpdfja" ( %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex cd %BUILDDIR%/latex make all-pdf-ja cd %~dp0 echo. echo.Build finished; the PDF files are in %BUILDDIR%/latex. goto end ) if "%1" == "text" ( %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text if errorlevel 1 exit /b 1 echo. echo.Build finished. The text files are in %BUILDDIR%/text. goto end ) if "%1" == "man" ( %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man if errorlevel 1 exit /b 1 echo. echo.Build finished. The manual pages are in %BUILDDIR%/man. goto end ) if "%1" == "texinfo" ( %SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo if errorlevel 1 exit /b 1 echo. echo.Build finished. The Texinfo files are in %BUILDDIR%/texinfo. goto end ) if "%1" == "gettext" ( %SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/locale if errorlevel 1 exit /b 1 echo. echo.Build finished. The message catalogs are in %BUILDDIR%/locale. goto end ) if "%1" == "changes" ( %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes if errorlevel 1 exit /b 1 echo. echo.The overview file is in %BUILDDIR%/changes. goto end ) if "%1" == "linkcheck" ( %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck if errorlevel 1 exit /b 1 echo. echo.Link check complete; look for any errors in the above output ^ or in %BUILDDIR%/linkcheck/output.txt. goto end ) if "%1" == "doctest" ( %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest if errorlevel 1 exit /b 1 echo. echo.Testing of doctests in the sources finished, look at the ^ results in %BUILDDIR%/doctest/output.txt. goto end ) if "%1" == "coverage" ( %SPHINXBUILD% -b coverage %ALLSPHINXOPTS% %BUILDDIR%/coverage if errorlevel 1 exit /b 1 echo. echo.Testing of coverage in the sources finished, look at the ^ results in %BUILDDIR%/coverage/python.txt. goto end ) if "%1" == "xml" ( %SPHINXBUILD% -b xml %ALLSPHINXOPTS% %BUILDDIR%/xml if errorlevel 1 exit /b 1 echo. echo.Build finished. The XML files are in %BUILDDIR%/xml. goto end ) if "%1" == "pseudoxml" ( %SPHINXBUILD% -b pseudoxml %ALLSPHINXOPTS% %BUILDDIR%/pseudoxml if errorlevel 1 exit /b 1 echo. echo.Build finished. The pseudo-XML files are in %BUILDDIR%/pseudoxml. goto end ) :end ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1744063414.9735498 acme-4.0.0/docs/man/0000775000175000017500000000000014775045667013230 5ustar00willgwillg././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1744063413.0 acme-4.0.0/docs/man/jws.rst0000664000175000017500000000005614775045665014564 0ustar00willgwillg:orphan: .. literalinclude:: ../jws-help.txt ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1744063414.9735498 acme-4.0.0/examples/0000775000175000017500000000000014775045667013343 5ustar00willgwillg././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1744063413.0 acme-4.0.0/examples/http01_example.py0000664000175000017500000001555614775045665016562 0ustar00willgwillg"""Example ACME-V2 API for HTTP-01 challenge. Brief: This a complete usage example of the python-acme API. Limitations of this example: - Works for only one Domain name - Performs only HTTP-01 challenge - Uses ACME-v2 Workflow: (Account creation) - Create account key - Register account and accept TOS (Certificate actions) - Select HTTP-01 within offered challenges by the CA server - Set up http challenge resource - Set up standalone web server - Create domain private key and CSR - Issue certificate - Renew certificate - Revoke certificate (Account update actions) - Change contact information - Deactivate Account """ from contextlib import contextmanager from cryptography.hazmat.backends import default_backend from cryptography.hazmat.primitives import serialization from cryptography.hazmat.primitives.asymmetric import rsa import josepy as jose import OpenSSL from acme import challenges from acme import client from acme import crypto_util from acme import errors from acme import messages from acme import standalone # Constants: # This is the staging point for ACME-V2 within Let's Encrypt. DIRECTORY_URL = 'https://acme-staging-v02.api.letsencrypt.org/directory' USER_AGENT = 'python-acme-example' # Account key size ACC_KEY_BITS = 2048 # Certificate private key size CERT_PKEY_BITS = 2048 # Domain name for the certificate. DOMAIN = 'client.example.com' # If you are running Boulder locally, it is possible to configure any port # number to execute the challenge, but real CA servers will always use port # 80, as described in the ACME specification. PORT = 80 # Useful methods and classes: def new_csr_comp(domain_name, pkey_pem=None): """Create certificate signing request.""" if pkey_pem is None: # Create private key. pkey = rsa.generate_private_key(public_exponent=65537, key_size=CERT_PKEY_BITS) pkey_pem = pkey.public_bytes(serialization.Encoding.PEM) csr_pem = crypto_util.make_csr(pkey_pem, [domain_name]) return pkey_pem, csr_pem def select_http01_chall(orderr): """Extract authorization resource from within order resource.""" # Authorization Resource: authz. # This object holds the offered challenges by the server and their status. authz_list = orderr.authorizations for authz in authz_list: # Choosing challenge. # authz.body.challenges is a set of ChallengeBody objects. for i in authz.body.challenges: # Find the supported challenge. if isinstance(i.chall, challenges.HTTP01): return i raise Exception('HTTP-01 challenge was not offered by the CA server.') @contextmanager def challenge_server(http_01_resources): """Manage standalone server set up and shutdown.""" # Setting up a fake server that binds at PORT and any address. address = ('', PORT) try: servers = standalone.HTTP01DualNetworkedServers(address, http_01_resources) # Start client standalone web server. servers.serve_forever() yield servers finally: # Shutdown client web server and unbind from PORT servers.shutdown_and_server_close() def perform_http01(client_acme, challb, orderr): """Set up standalone webserver and perform HTTP-01 challenge.""" response, validation = challb.response_and_validation(client_acme.net.key) resource = standalone.HTTP01RequestHandler.HTTP01Resource( chall=challb.chall, response=response, validation=validation) with challenge_server({resource}): # Let the CA server know that we are ready for the challenge. client_acme.answer_challenge(challb, response) # Wait for challenge status and then issue a certificate. # It is possible to set a deadline time. finalized_orderr = client_acme.poll_and_finalize(orderr) return finalized_orderr.fullchain_pem # Main examples: def example_http(): """This example executes the whole process of fulfilling a HTTP-01 challenge for one specific domain. The workflow consists of: (Account creation) - Create account key - Register account and accept TOS (Certificate actions) - Select HTTP-01 within offered challenges by the CA server - Set up http challenge resource - Set up standalone web server - Create domain private key and CSR - Issue certificate - Renew certificate - Revoke certificate (Account update actions) - Change contact information - Deactivate Account """ # Create account key acc_key = jose.JWKRSA( key=rsa.generate_private_key(public_exponent=65537, key_size=ACC_KEY_BITS, backend=default_backend())) # Register account and accept TOS net = client.ClientNetwork(acc_key, user_agent=USER_AGENT) directory = client.ClientV2.get_directory(DIRECTORY_URL, net) client_acme = client.ClientV2(directory, net=net) # Terms of Service URL is in client_acme.directory.meta.terms_of_service # Registration Resource: regr # Creates account with contact information. email = ('fake@example.com') regr = client_acme.new_account( messages.NewRegistration.from_data( email=email, terms_of_service_agreed=True)) # Create domain private key and CSR pkey_pem, csr_pem = new_csr_comp(DOMAIN) # Issue certificate orderr = client_acme.new_order(csr_pem) # Select HTTP-01 within offered challenges by the CA server challb = select_http01_chall(orderr) # The certificate is ready to be used in the variable "fullchain_pem". fullchain_pem = perform_http01(client_acme, challb, orderr) # Renew certificate _, csr_pem = new_csr_comp(DOMAIN, pkey_pem) orderr = client_acme.new_order(csr_pem) challb = select_http01_chall(orderr) # Performing challenge fullchain_pem = perform_http01(client_acme, challb, orderr) # Revoke certificate fullchain_com = x509.load_pem_x509_certificate(fullchain_pem) try: client_acme.revoke(fullchain_com, 0) # revocation reason = 0 except errors.ConflictError: # Certificate already revoked. pass # Query registration status. client_acme.net.account = regr try: regr = client_acme.query_registration(regr) except errors.Error as err: if err.typ == messages.ERROR_PREFIX + 'unauthorized': # Status is deactivated. pass raise # Change contact information email = 'newfake@example.com' regr = client_acme.update_registration( regr.update( body=regr.body.update( contact=('mailto:' + email,) ) ) ) # Deactivate account/registration regr = client_acme.deactivate_registration(regr) if __name__ == "__main__": example_http() ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1744063413.0 acme-4.0.0/pytest.ini0000664000175000017500000000007714775045665013560 0ustar00willgwillg[pytest] norecursedirs = .* build dist CVS _darcs {arch} *.egg ././@PaxHeader0000000000000000000000000000003200000000000010210 xustar0026 mtime=1744063414.97455 acme-4.0.0/setup.cfg0000664000175000017500000000004614775045667013346 0ustar00willgwillg[egg_info] tag_build = tag_date = 0 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1744063413.0 acme-4.0.0/setup.py0000664000175000017500000000315614775045665013242 0ustar00willgwillgfrom setuptools import find_packages from setuptools import setup version = '4.0.0' install_requires = [ 'cryptography>=43.0.0', 'josepy>=2.0.0', # PyOpenSSL>=25.0.0 is just needed to satisfy mypy right now so this dependency can probably be # relaxed to >=24.0.0 if needed. 'PyOpenSSL>=25.0.0', 'pyrfc3339', 'pytz>=2019.3', 'requests>=2.20.0', ] docs_extras = [ 'Sphinx>=1.0', # autodoc_member_order = 'bysource', autodoc_default_flags 'sphinx_rtd_theme', ] test_extras = [ 'pytest', 'pytest-xdist', 'typing-extensions', ] setup( name='acme', version=version, description='ACME protocol implementation in Python', url='https://github.com/certbot/certbot', author="Certbot Project", author_email='certbot-dev@eff.org', license='Apache License 2.0', python_requires='>=3.9', classifiers=[ 'Development Status :: 5 - Production/Stable', 'Intended Audience :: Developers', 'License :: OSI Approved :: Apache Software License', 'Programming Language :: Python', 'Programming Language :: Python :: 3', 'Programming Language :: Python :: 3.9', 'Programming Language :: Python :: 3.10', 'Programming Language :: Python :: 3.11', 'Programming Language :: Python :: 3.12', 'Programming Language :: Python :: 3.13', 'Topic :: Internet :: WWW/HTTP', 'Topic :: Security', ], packages=find_packages(), include_package_data=True, install_requires=install_requires, extras_require={ 'docs': docs_extras, 'test': test_extras, }, )