././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1635888462.3090627 acme-1.21.0/0000755000076500000240000000000000000000000011151 5ustar00bmwstaff././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1635888438.0 acme-1.21.0/LICENSE.txt0000644000076500000240000002504200000000000012777 0ustar00bmwstaff 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=1635888438.0 acme-1.21.0/MANIFEST.in0000644000076500000240000000027600000000000012714 0ustar00bmwstaffinclude LICENSE.txt include README.rst include pytest.ini recursive-include docs * recursive-include examples * recursive-include tests * global-exclude __pycache__ global-exclude *.py[cod] ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1635888462.3091168 acme-1.21.0/PKG-INFO0000644000076500000240000000156200000000000012252 0ustar00bmwstaffMetadata-Version: 2.1 Name: acme Version: 1.21.0 Summary: ACME protocol implementation in Python Home-page: https://github.com/letsencrypt/letsencrypt Author: Certbot Project Author-email: certbot-dev@eff.org License: Apache License 2.0 Platform: UNKNOWN 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.6 Classifier: Programming Language :: Python :: 3.7 Classifier: Programming Language :: Python :: 3.8 Classifier: Programming Language :: Python :: 3.9 Classifier: Topic :: Internet :: WWW/HTTP Classifier: Topic :: Security Requires-Python: >=3.6 Provides-Extra: docs Provides-Extra: test License-File: LICENSE.txt UNKNOWN ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1635888438.0 acme-1.21.0/README.rst0000644000076500000240000000004700000000000012641 0ustar00bmwstaffACME protocol implementation in Python ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1635888462.3024256 acme-1.21.0/acme/0000755000076500000240000000000000000000000012056 5ustar00bmwstaff././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1635888438.0 acme-1.21.0/acme/__init__.py0000644000076500000240000000132600000000000014171 0ustar00bmwstaff"""ACME protocol implementation. This module is an implementation of the `ACME protocol`_. .. _`ACME protocol`: https://ietf-wg-acme.github.io/acme """ 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] ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1635888438.0 acme-1.21.0/acme/challenges.py0000644000076500000240000005230500000000000014542 0ustar00bmwstaff"""ACME Identifier Validation Challenges.""" import abc import codecs import functools import hashlib import logging import socket from typing import cast from typing import Any from typing import Dict from typing import Mapping from typing import Optional from typing import Tuple from typing import Type 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 from acme import fields from acme.mixins import ResourceMixin from acme.mixins import TypeMixin logger = logging.getLogger(__name__) class Challenge(jose.TypedJSONObjectWithFields): # _fields_to_partial_json """ACME challenge.""" TYPES: Dict[str, Type['Challenge']] = {} @classmethod def from_json(cls, jobj: Mapping[str, Any]) -> 'Challenge': try: return super().from_json(jobj) except jose.UnrecognizedTypeError as error: logger.debug(error) return UnrecognizedChallenge.from_json(jobj) class ChallengeResponse(ResourceMixin, TypeMixin, jose.TypedJSONObjectWithFields): # _fields_to_partial_json """ACME challenge response.""" TYPES: Dict[str, Type['ChallengeResponse']] = {} resource_type = 'challenge' resource = fields.Resource(resource_type) 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. """ 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 unicode key_authorization: """ key_authorization = 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('.') 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 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 unicode: """ 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 unicode 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: unicode """ 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 unicode name: Domain name being validated. """ return "{0}.{1}".format(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) -> bool: """Simple verify. :param challenges.SimpleHTTP chall: Corresponding challenge. :param unicode 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. :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) 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: string """ 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 unicode domain: Domain name being verified. :rtype: string """ return "http://" + domain + self.path def validation(self, account_key: jose.JWK, **unused_kwargs: Any) -> str: """Generate validation. :param JWK account_key: :rtype: unicode """ 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 = "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[crypto.X509, crypto.PKey]: """Generate tls-alpn-01 certificate. :param unicode 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 `OpenSSL.crypto.X509` and `OpenSSL.crypto.PKey` """ if key is None: key = crypto.PKey() key.generate_key(crypto.TYPE_RSA, bits) der_value = b"DER:" + codecs.encode(self.h, 'hex') acme_extension = crypto.X509Extension(self.ID_PE_ACME_IDENTIFIER_V1, critical=True, value=der_value) return crypto_util.gen_ss_cert(key, [domain], force_san=True, extensions=[acme_extension]), key def probe_cert(self, domain: str, host: Optional[str] = None, port: Optional[int] = None) -> crypto.X509: """Probe tls-alpn-01 challenge certificate. :param unicode domain: domain being validated, required. :param string 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: crypto.X509) -> bool: """Verify tls-alpn-01 challenge certificate. :param unicode domain: Domain name being validated. :param OpensSSL.crypto.X509 cert: Challenge certificate. :returns: Whether the certificate was successfully verified. :rtype: bool """ # pylint: disable=protected-access names = crypto_util._pyopenssl_cert_or_req_all_names(cert) # Type ignore needed due to # https://github.com/pyca/pyopenssl/issues/730. logger.debug('Certificate %s. SANs: %s', cert.digest('sha256'), names) # type: ignore[arg-type] if len(names) != 1 or names[0].lower() != domain.lower(): return False for i in range(cert.get_extension_count()): ext = cert.get_extension(i) # FIXME: assume this is the ACME extension. Currently there is no # way to get full OID of an unknown extension from pyopenssl. if ext.get_short_name() == b'UNDEF': data = ext.get_data() return data == self.h return False # pylint: disable=too-many-arguments def simple_verify(self, chall: 'TLSALPN01', domain: str, account_public_key: jose.JWK, cert: Optional[crypto.X509] = 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 OpenSSL.crypto.X509 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[crypto.X509, crypto.PKey]: """Generate validation. :param JWK account_key: :param unicode 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 `OpenSSL.crypto.X509` and `OpenSSL.crypto.PKey` """ return self.response(account_key).gen_cert( key=kwargs.get('cert_key'), domain=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 unicode 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.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(cast(jose.JWS, self.validation), account_public_key) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1635888438.0 acme-1.21.0/acme/client.py0000644000076500000240000015156500000000000013723 0ustar00bmwstaff"""ACME client API.""" # pylint: disable=too-many-lines # This pylint disable can be deleted once the deprecated ACMEv1 code is # removed. import base64 import collections import datetime from email.utils import parsedate_tz import heapq import http.client as http_client import logging import re import sys import time from types import ModuleType from typing import Any from typing import Callable from typing import cast from typing import Dict from typing import Iterable from typing import List from typing import Optional from typing import Set from typing import Text from typing import Tuple from typing import Union import warnings import josepy as jose import OpenSSL import requests from requests.adapters import HTTPAdapter from requests.utils import parse_header_links from requests_toolbelt.adapters.source import SourceAddressAdapter from acme import crypto_util from acme import errors from acme import jws from acme import messages from acme.mixins import VersionedLEACMEMixin logger = logging.getLogger(__name__) DEFAULT_NETWORK_TIMEOUT = 45 DER_CONTENT_TYPE = 'application/pkix-cert' class ClientBase: """ACME client base object. :ivar messages.Directory directory: :ivar .ClientNetwork net: Client network. :ivar int acme_version: ACME protocol version. 1 or 2. """ def __init__(self, directory: messages.Directory, net: 'ClientNetwork', acme_version: int) -> None: """Initialize. :param .messages.Directory directory: Directory Resource :param .ClientNetwork net: Client network. :param int acme_version: ACME protocol version. 1 or 2. """ self.directory = directory self.net = net self.acme_version = acme_version @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 acme_version. """ kwargs.setdefault('acme_version', self.acme_version) if hasattr(self.directory, 'newNonce'): kwargs.setdefault('new_nonce_url', getattr(self.directory, 'newNonce')) return self.net.post(*args, **kwargs) 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` """ 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 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: raise errors.UnexpectedUpdate(authzr) return authzr def answer_challenge(self, challb: messages.ChallengeBody, response: requests.Response ) -> 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: """ response = self._post(challb.uri, response) try: authzr_uri = response.links['up']['url'] except KeyError: raise errors.ClientError('"up" Link header missing') challr = messages.ChallengeResource( authzr_uri=authzr_uri, body=messages.ChallengeBody.from_json(response.json())) # TODO: check that challr.uri == response.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: jose.ComparableX509, rsn: int, url: str) -> None: """Revoke certificate. :param .ComparableX509 cert: `OpenSSL.crypto.X509` wrapped in `.ComparableX509` :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 Client(ClientBase): """ACME client for a v1 API. .. deprecated:: 1.18.0 Use :class:`ClientV2` instead. .. todo:: Clean up raised error types hierarchy, document, and handle (wrap) instances of `.DeserializationError` raised in `from_json()`. :ivar messages.Directory directory: :ivar key: `josepy.JWK` (private) :ivar alg: `josepy.JWASignature` :ivar bool verify_ssl: Verify SSL certificates? :ivar .ClientNetwork net: Client network. Useful for testing. If not supplied, it will be initialized using `key`, `alg` and `verify_ssl`. """ def __init__(self, directory: messages.Directory, key: jose.JWK, alg: jose.JWASignature=jose.RS256, verify_ssl: bool = True, net: Optional['ClientNetwork'] = None) -> None: """Initialize. :param directory: Directory Resource (`.messages.Directory`) or URI from which the resource will be downloaded. """ self.key = key if net is None: net = ClientNetwork(key, alg=alg, verify_ssl=verify_ssl) if isinstance(directory, str): directory = messages.Directory.from_json( net.get(directory).json()) super().__init__(directory=directory, net=net, acme_version=1) def register(self, new_reg: Optional[messages.NewRegistration] = None ) -> messages.RegistrationResource: """Register. :param .NewRegistration new_reg: :returns: Registration Resource. :rtype: `.RegistrationResource` """ new_reg = messages.NewRegistration() if new_reg is None else new_reg response = self._post(self.directory[new_reg], new_reg) # TODO: handle errors assert response.status_code == http_client.CREATED # "Instance of 'Field' has no key/contact member" bug: return self._regr_from_response(response) def query_registration(self, regr: messages.RegistrationResource ) -> messages.RegistrationResource: """Query server about registration. :param messages.RegistrationResource regr: Existing Registration Resource. """ return self._send_recv_regr(regr, messages.UpdateRegistration()) def agree_to_tos(self, regr: messages.RegistrationResource ) -> messages.RegistrationResource: """Agree to the terms-of-service. Agree to the terms-of-service in a Registration Resource. :param regr: Registration Resource. :type regr: `.RegistrationResource` :returns: Updated Registration Resource. :rtype: `.RegistrationResource` """ return self.update_registration( regr.update(body=regr.body.update(agreement=regr.terms_of_service))) def request_challenges(self, identifier: messages.Identifier, new_authzr_uri: Optional[str] = None) -> messages.AuthorizationResource: """Request challenges. :param .messages.Identifier identifier: Identifier to be challenged. :param str new_authzr_uri: Deprecated. Do not use. :returns: Authorization Resource. :rtype: `.AuthorizationResource` :raises errors.WildcardUnsupportedError: if a wildcard is requested """ if new_authzr_uri is not None: logger.debug("request_challenges with new_authzr_uri deprecated.") if identifier.value.startswith("*"): raise errors.WildcardUnsupportedError( "Requesting an authorization for a wildcard name is" " forbidden by this version of the ACME protocol.") new_authz = messages.NewAuthorization(identifier=identifier) response = self._post(self.directory.new_authz, new_authz) # TODO: handle errors assert response.status_code == http_client.CREATED return self._authzr_from_response(response, identifier) def request_domain_challenges(self, domain: str,new_authzr_uri: Optional[str] = None ) -> messages.AuthorizationResource: """Request challenges for domain names. This is simply a convenience function that wraps around `request_challenges`, but works with domain names instead of generic identifiers. See ``request_challenges`` for more documentation. :param str domain: Domain name to be challenged. :param str new_authzr_uri: Deprecated. Do not use. :returns: Authorization Resource. :rtype: `.AuthorizationResource` :raises errors.WildcardUnsupportedError: if a wildcard is requested """ return self.request_challenges(messages.Identifier( typ=messages.IDENTIFIER_FQDN, value=domain), new_authzr_uri) def request_issuance(self, csr: jose.ComparableX509, authzrs: Iterable[messages.AuthorizationResource] ) -> messages.CertificateResource: """Request issuance. :param csr: CSR :type csr: `OpenSSL.crypto.X509Req` wrapped in `.ComparableX509` :param authzrs: `list` of `.AuthorizationResource` :returns: Issued certificate :rtype: `.messages.CertificateResource` """ assert authzrs, "Authorizations list is empty" logger.debug("Requesting issuance...") # TODO: assert len(authzrs) == number of SANs req = messages.CertificateRequest(csr=csr) content_type = DER_CONTENT_TYPE # TODO: add 'cert_type 'argument response = self._post( self.directory.new_cert, req, content_type=content_type, headers={'Accept': content_type}) cert_chain_uri = response.links.get('up', {}).get('url') try: uri = response.headers['Location'] except KeyError: raise errors.ClientError('"Location" Header missing') return messages.CertificateResource( uri=uri, authzrs=authzrs, cert_chain_uri=cert_chain_uri, body=jose.ComparableX509(OpenSSL.crypto.load_certificate( OpenSSL.crypto.FILETYPE_ASN1, response.content))) 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.net.get(authzr.uri) updated_authzr = self._authzr_from_response( response, authzr.body.identifier, authzr.uri) return updated_authzr, response def poll_and_request_issuance(self, csr: jose.ComparableX509, authzrs: Iterable[messages.AuthorizationResource], mintime: int = 5, max_attempts: int = 10 ) -> Tuple[messages.CertificateResource, Tuple[messages.AuthorizationResource, ...]]: """Poll and request issuance. This function polls all provided Authorization Resource URIs until all challenges are valid, respecting ``Retry-After`` HTTP headers, and then calls `request_issuance`. :param .ComparableX509 csr: CSR (`OpenSSL.crypto.X509Req` wrapped in `.ComparableX509`) :param authzrs: `list` of `.AuthorizationResource` :param int mintime: Minimum time before next attempt, used if ``Retry-After`` is not present in the response. :param int max_attempts: Maximum number of attempts (per authorization) before `PollError` with non-empty ``waiting`` is raised. :returns: ``(cert, updated_authzrs)`` `tuple` where ``cert`` is the issued certificate (`.messages.CertificateResource`), and ``updated_authzrs`` is a `tuple` consisting of updated Authorization Resources (`.AuthorizationResource`) as present in the responses from server, and in the same order as the input ``authzrs``. :rtype: `tuple` :raises PollError: in case of timeout or if some authorization was marked by the CA as invalid """ assert max_attempts > 0 attempts: Dict[messages.AuthorizationResource, int] = collections.defaultdict(int) exhausted = set() # priority queue with datetime.datetime (based on Retry-After) as key, # and original Authorization Resource as value waiting = [ (datetime.datetime.now(), index, authzr) for index, authzr in enumerate(authzrs) ] heapq.heapify(waiting) # mapping between original Authorization Resource and the most # recently updated one updated = {authzr: authzr for authzr in authzrs} while waiting: # find the smallest Retry-After, and sleep if necessary when, index, authzr = heapq.heappop(waiting) now = datetime.datetime.now() if when > now: seconds = (when - now).seconds logger.debug('Sleeping for %d seconds', seconds) time.sleep(seconds) # Note that we poll with the latest updated Authorization # URI, which might have a different URI than initial one updated_authzr, response = self.poll(updated[authzr]) updated[authzr] = updated_authzr attempts[authzr] += 1 if updated_authzr.body.status not in ( messages.STATUS_VALID, messages.STATUS_INVALID): if attempts[authzr] < max_attempts: # push back to the priority queue, with updated retry_after heapq.heappush(waiting, (self.retry_after( response, default=mintime), index, authzr)) else: exhausted.add(authzr) if exhausted or any(authzr.body.status == messages.STATUS_INVALID for authzr in updated.values()): raise errors.PollError(exhausted, updated) updated_authzrs = tuple(updated[authzr] for authzr in authzrs) return self.request_issuance(csr, updated_authzrs), updated_authzrs def _get_cert(self, uri: str) -> Tuple[requests.Response, jose.ComparableX509]: """Returns certificate from URI. :param str uri: URI of certificate :returns: tuple of the form (response, :class:`josepy.util.ComparableX509`) :rtype: tuple """ content_type = DER_CONTENT_TYPE # TODO: make it a param response = self.net.get(uri, headers={'Accept': content_type}, content_type=content_type) return response, jose.ComparableX509(OpenSSL.crypto.load_certificate( OpenSSL.crypto.FILETYPE_ASN1, response.content)) def check_cert(self, certr: messages.CertificateResource) -> messages.CertificateResource: """Check for new cert. :param certr: Certificate Resource :type certr: `.CertificateResource` :returns: Updated Certificate Resource. :rtype: `.CertificateResource` """ # TODO: acme-spec 5.1 table action should be renamed to # "refresh cert", and this method integrated with self.refresh response, cert = self._get_cert(certr.uri) if 'Location' not in response.headers: raise errors.ClientError('Location header missing') if response.headers['Location'] != certr.uri: raise errors.UnexpectedUpdate(response.text) return certr.update(body=cert) def refresh(self, certr: messages.CertificateResource) -> messages.CertificateResource: """Refresh certificate. :param certr: Certificate Resource :type certr: `.CertificateResource` :returns: Updated Certificate Resource. :rtype: `.CertificateResource` """ # TODO: If a client sends a refresh request and the server is # not willing to refresh the certificate, the server MUST # respond with status code 403 (Forbidden) return self.check_cert(certr) def fetch_chain(self, certr: messages.CertificateResource, max_length: int = 10) -> List[jose.ComparableX509]: """Fetch chain for certificate. :param .CertificateResource certr: Certificate Resource :param int max_length: Maximum allowed length of the chain. Note that each element in the certificate requires new ``HTTP GET`` request, and the length of the chain is controlled by the ACME CA. :raises errors.Error: if recursion exceeds `max_length` :returns: Certificate chain for the Certificate Resource. It is a list ordered so that the first element is a signer of the certificate from Certificate Resource. Will be empty if ``cert_chain_uri`` is ``None``. :rtype: `list` of `OpenSSL.crypto.X509` wrapped in `.ComparableX509` """ chain: List[jose.ComparableX509] = [] uri = certr.cert_chain_uri while uri is not None and len(chain) < max_length: response, cert = self._get_cert(uri) uri = response.links.get('up', {}).get('url') chain.append(cert) if uri is not None: raise errors.Error( "Recursion limit reached. Didn't get {0}".format(uri)) return chain def revoke(self, cert: jose.ComparableX509, rsn: int) -> None: """Revoke certificate. :param .ComparableX509 cert: `OpenSSL.crypto.X509` wrapped in `.ComparableX509` :param int rsn: Reason code for certificate revocation. :raises .ClientError: If revocation is unsuccessful. """ self._revoke(cert, rsn, self.directory[cast(str, messages.Revocation)]) class ClientV2(ClientBase): """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. """ super().__init__(directory=directory, net=net, acme_version=2) 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 = regr # See certbot/certbot#6258 # ACME v2 requires to use a POST-as-GET request (POST an empty JWS) here. # This is done by passing None instead of an empty UpdateRegistration to _post(). response = self._post(regr.uri, None) self.net.account = self._regr_from_response(response, uri=regr.uri, terms_of_service=regr.terms_of_service) 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 new_regr = self._get_v2_account(regr) return super().update_registration(new_regr, update) def _get_v2_account(self, regr: messages.RegistrationResource) -> 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(uri=updated_uri) self.net.account = new_regr return new_regr def new_order(self, csr_pem: bytes) -> 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 = OpenSSL.crypto.load_certificate_request(OpenSSL.crypto.FILETYPE_PEM, csr_pem) # pylint: disable=protected-access dnsNames = crypto_util._pyopenssl_cert_or_req_all_names(csr) ipNames = crypto_util._pyopenssl_cert_or_req_san_ip(csr) # ipNames is now []string identifiers = [] for name in dnsNames: identifiers.append(messages.Identifier(typ=messages.IDENTIFIER_FQDN, value=name)) for ips in ipNames: identifiers.append(messages.Identifier(typ=messages.IDENTIFIER_IP, value=ips)) order = messages.NewOrder(identifiers=identifiers) 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: 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 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 """ csr = OpenSSL.crypto.load_certificate_request( OpenSSL.crypto.FILETYPE_PEM, orderr.csr_pem) wrapped_csr = messages.CertificateRequest(csr=jose.ComparableX509(csr)) self._post(orderr.body.finalize, wrapped_csr) while datetime.datetime.now() < deadline: time.sleep(1) response = self._post_as_get(orderr.uri) body = messages.Order.from_json(response.json()) if body.error is not None: raise errors.IssuanceError(body.error) if 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 revoke(self, cert: jose.ComparableX509, rsn: int) -> None: """Revoke certificate. :param .ComparableX509 cert: `OpenSSL.crypto.X509` wrapped in `.ComparableX509` :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 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] class BackwardsCompatibleClientV2: """ACME client wrapper that tends towards V2-style calls, but supports V1 servers. .. deprecated:: 1.18.0 Use :class:`ClientV2` instead. .. note:: While this class handles the majority of the differences between versions of the ACME protocol, if you need to support an ACME server based on version 3 or older of the IETF ACME draft that uses combinations in authorizations (or lack thereof) to signal that the client needs to complete something other than any single challenge in the authorization to make it valid, the user of this class needs to understand and handle these differences themselves. This does not apply to either of Let's Encrypt's endpoints where successfully completing any challenge in an authorization will make it valid. :ivar int acme_version: 1 or 2, corresponding to the Let's Encrypt endpoint :ivar .ClientBase client: either Client or ClientV2 """ def __init__(self, net: 'ClientNetwork', key: jose.JWK, server: str) -> None: directory = messages.Directory.from_json(net.get(server).json()) self.acme_version = self._acme_version_from_directory(directory) self.client: Union[Client, ClientV2] if self.acme_version == 1: self.client = Client(directory, key=key, net=net) else: self.client = ClientV2(directory, net=net) def __getattr__(self, name: str) -> Any: return getattr(self.client, name) def new_account_and_tos(self, regr: messages.NewRegistration, check_tos_cb: Optional[Callable[[str], None]] = None ) -> messages.RegistrationResource: """Combined register and agree_tos for V1, new_account for V2 :param .NewRegistration regr: :param callable check_tos_cb: callback that raises an error if the check does not work """ def _assess_tos(tos: str) -> None: if check_tos_cb is not None: check_tos_cb(tos) if self.acme_version == 1: client_v1 = cast(Client, self.client) regr = client_v1.register(regr) if regr.terms_of_service is not None: _assess_tos(regr.terms_of_service) return client_v1.agree_to_tos(regr) return regr else: client_v2 = cast(ClientV2, self.client) if "terms_of_service" in client_v2.directory.meta: _assess_tos(client_v2.directory.meta.terms_of_service) regr = regr.update(terms_of_service_agreed=True) return client_v2.new_account(regr) def new_order(self, csr_pem: bytes) -> messages.OrderResource: """Request a new Order object from the server. If using ACMEv1, returns a dummy OrderResource with only the authorizations field filled in. :param bytes csr_pem: A CSR in PEM format. :returns: The newly created order. :rtype: OrderResource :raises errors.WildcardUnsupportedError: if a wildcard domain is requested but unsupported by the ACME version """ if self.acme_version == 1: client_v1 = cast(Client, self.client) csr = OpenSSL.crypto.load_certificate_request(OpenSSL.crypto.FILETYPE_PEM, csr_pem) # pylint: disable=protected-access dnsNames = crypto_util._pyopenssl_cert_or_req_all_names(csr) authorizations = [] for domain in dnsNames: authorizations.append(client_v1.request_domain_challenges(domain)) return messages.OrderResource(authorizations=authorizations, csr_pem=csr_pem) return cast(ClientV2, self.client).new_order(csr_pem) 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 """ if self.acme_version == 1: client_v1 = cast(Client, self.client) csr_pem = orderr.csr_pem certr = client_v1.request_issuance( jose.ComparableX509( OpenSSL.crypto.load_certificate_request(OpenSSL.crypto.FILETYPE_PEM, csr_pem)), orderr.authorizations) chain = None while datetime.datetime.now() < deadline: try: chain = client_v1.fetch_chain(certr) break except errors.Error: time.sleep(1) if chain is None: raise errors.TimeoutError( 'Failed to fetch chain. You should not deploy the generated ' 'certificate, please rerun the command for a new one.') cert = OpenSSL.crypto.dump_certificate( OpenSSL.crypto.FILETYPE_PEM, certr.body.wrapped).decode() chain_str = crypto_util.dump_pyopenssl_chain(chain).decode() return orderr.update(fullchain_pem=(cert + chain_str)) return cast(ClientV2, self.client).finalize_order( orderr, deadline, fetch_alternative_chains) def revoke(self, cert: jose.ComparableX509, rsn: int) -> None: """Revoke certificate. :param .ComparableX509 cert: `OpenSSL.crypto.X509` wrapped in `.ComparableX509` :param int rsn: Reason code for certificate revocation. :raises .ClientError: If revocation is unsuccessful. """ self.client.revoke(cert, rsn) def _acme_version_from_directory(self, directory: messages.Directory) -> int: if hasattr(directory, 'newNonce'): return 2 return 1 def external_account_required(self) -> bool: """Checks if the server requires an external account for ACMEv2 servers. Always return False for ACMEv1 servers, as it doesn't use External Account Binding.""" if self.acme_version == 1: return False return cast(ClientV2, self.client).external_account_required() 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() with acme_version=2 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 float timeout: Timeout for requests. :param source_address: Optional source address to bind to when making requests. :type source_address: str or tuple(str, int) """ 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, source_address: Optional[Union[str, Tuple[str, int]]] = None) -> None: self.key = key self.account = account self.alg = alg self.verify_ssl = verify_ssl self._nonces: Set[Text] = set() self.user_agent = user_agent self.session = requests.Session() self._default_timeout = timeout adapter = HTTPAdapter() if source_address is not None: adapter = SourceAddressAdapter(source_address) 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, acme_version: int) -> jose.JWS: """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: `josepy.JWS` """ if isinstance(obj, VersionedLEACMEMixin): obj.le_acme_version = acme_version jobj = obj.json_dumps(indent=2).encode() if obj else b'' logger.debug('JWS payload:\n%s', jobj) kwargs = { "alg": self.alg, "nonce": nonce } if acme_version == 2: kwargs["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, **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 (draft-ietf-appsawg-http-problem-00). :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( 'Unexpected response Content-Type: {0}'.format(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("Requesting {0}{1}:{2}".format(host, path, err_msg)) # If the Content-Type is DER or 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. This code can be # simplified to only check for an Accept header in the request when # ACMEv1 support is dropped. debug_content: Union[bytes, str] if (response.headers.get("Content-Type") == DER_CONTENT_TYPE or "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, acme_version: int = 1, **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, acme_version) 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 # This class takes a similar approach to the cryptography project to deprecate attributes # in public modules. See the _ModuleWithDeprecation class here: # https://github.com/pyca/cryptography/blob/91105952739442a74582d3e62b3d2111365b0dc7/src/cryptography/utils.py#L129 class _ClientDeprecationModule: """ Internal class delegating to a module, and displaying warnings when attributes related to deprecated attributes in the acme.client module. """ def __init__(self, module: ModuleType) -> None: self.__dict__['_module'] = module def __getattr__(self, attr: str) -> Any: if attr in ('Client', 'BackwardsCompatibleClientV2'): warnings.warn('The {0} attribute in acme.client is deprecated ' 'and will be removed soon.'.format(attr), DeprecationWarning, stacklevel=2) return getattr(self._module, attr) def __setattr__(self, attr: str, value: Any) -> None: # pragma: no cover setattr(self._module, attr, value) def __delattr__(self, attr: str) -> None: # pragma: no cover delattr(self._module, attr) def __dir__(self) -> List[str]: # pragma: no cover return ['_module'] + dir(self._module) # Patching ourselves to warn about deprecation and planned removal of some elements in the module. sys.modules[__name__] = cast(ModuleType, _ClientDeprecationModule(sys.modules[__name__])) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1635888438.0 acme-1.21.0/acme/crypto_util.py0000644000076500000240000004053600000000000015015 0ustar00bmwstaff"""Crypto utilities.""" import binascii import contextlib import ipaddress import logging import os import re import socket from typing import Any from typing import Callable from typing import List from typing import Mapping from typing import Optional from typing import Set from typing import Tuple from typing import Union import josepy as jose 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 SSLv23_METHOD is compatible with all other # methods, including TLSv2_METHOD (read more at # https://www.openssl.org/docs/ssl/SSLv23_method.html). _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.SSLv23_METHOD class _DefaultCertSelection: def __init__(self, certs: Mapping[bytes, Tuple[crypto.PKey, crypto.X509]]): self.certs = certs def __call__(self, connection: SSL.Connection) -> Optional[Tuple[crypto.PKey, crypto.X509]]: server_name = connection.get_servername() return self.certs.get(server_name, None) 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, Tuple[crypto.PKey, crypto.X509]]] = None, method: int = _DEFAULT_SSL_METHOD, alpn_selection: Optional[Callable[[SSL.Connection, List[bytes]], bytes]] = None, cert_selection: Optional[Callable[[SSL.Connection], Tuple[crypto.PKey, crypto.X509]]] = 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.") actual_cert_selection: Union[_DefaultCertSelection, Optional[Callable[[SSL.Connection], Tuple[crypto.PKey, crypto.X509]]]] = cert_selection if actual_cert_selection is None: actual_cert_selection = _DefaultCertSelection(certs if certs else {}) self.cert_selection = actual_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_options(SSL.OP_NO_SSLv2) new_context.set_options(SSL.OP_NO_SSLv3) new_context.use_privatekey(key) 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 return self._wrapped.shutdown() def accept(self) -> Tuple[FakeConnection, Any]: # pylint: disable=missing-function-docstring sock, addr = self.sock.accept() 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() 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 socket.error(error) return ssl_sock, addr 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[List[str]] = None) -> crypto.X509: """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: `list` of `str` :raises acme.errors.Error: In case of any problems. :returns: SSL certificate presented by the server. :rtype: OpenSSL.crypto.X509 """ 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 socket.error 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(alpn_protocols) try: client_ssl.do_handshake() client_ssl.shutdown() except SSL.Error as error: raise errors.Error(error) return client_ssl.get_peer_certificate() 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. :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. params ordered this way for backward competablity when called by positional argument. :returns: buffer PEM-encoded Certificate Signing Request. """ private_key = crypto.load_privatekey( crypto.FILETYPE_PEM, private_key_pem) csr = crypto.X509Req() sanlist = [] # if domain or ip list not supplied make it empty list so it's easier to iterate 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") for address in domains: sanlist.append('DNS:' + address) for ips in ipaddrs: sanlist.append('IP:' + ips.exploded) # make sure its ascii encoded san_string = ', '.join(sanlist).encode('ascii') # for IP san it's actually need to be octet-string, # but somewhere downsteam thankfully handle it for us extensions = [ crypto.X509Extension( b'subjectAltName', critical=False, value=san_string ), ] if must_staple: extensions.append(crypto.X509Extension( b"1.3.6.1.5.5.7.1.24", critical=False, value=b"DER:30:03:02:01:05")) csr.add_extensions(extensions) csr.set_pubkey(private_key) csr.set_version(2) csr.sign(private_key, 'sha256') return crypto.dump_certificate_request( crypto.FILETYPE_PEM, csr) def _pyopenssl_cert_or_req_all_names(loaded_cert_or_req: Union[crypto.X509, crypto.X509Req] ) -> List[str]: # unlike its name this only outputs DNS names, other type of idents will ignored common_name = loaded_cert_or_req.get_subject().CN sans = _pyopenssl_cert_or_req_san(loaded_cert_or_req) if common_name is None: return sans return [common_name] + [d for d in sans if d != common_name] def _pyopenssl_cert_or_req_san(cert_or_req: Union[crypto.X509, crypto.X509Req]) -> List[str]: """Get Subject Alternative Names from certificate or CSR using pyOpenSSL. .. todo:: Implement directly in PyOpenSSL! .. note:: Although this is `acme` internal API, it is used by `letsencrypt`. :param cert_or_req: Certificate or CSR. :type cert_or_req: `OpenSSL.crypto.X509` or `OpenSSL.crypto.X509Req`. :returns: A list of Subject Alternative Names that is DNS. :rtype: `list` of `unicode` """ # This function finds SANs with dns name # constants based on PyOpenSSL certificate/CSR text dump part_separator = ":" prefix = "DNS" + part_separator sans_parts = _pyopenssl_extract_san_list_raw(cert_or_req) return [part.split(part_separator)[1] for part in sans_parts if part.startswith(prefix)] def _pyopenssl_cert_or_req_san_ip(cert_or_req: Union[crypto.X509, crypto.X509Req]) -> List[str]: """Get Subject Alternative Names IPs from certificate or CSR using pyOpenSSL. :param cert_or_req: Certificate or CSR. :type cert_or_req: `OpenSSL.crypto.X509` or `OpenSSL.crypto.X509Req`. :returns: A list of Subject Alternative Names that are IP Addresses. :rtype: `list` of `unicode`. note that this returns as string, not IPaddress object """ # constants based on PyOpenSSL certificate/CSR text dump part_separator = ":" prefix = "IP Address" + part_separator sans_parts = _pyopenssl_extract_san_list_raw(cert_or_req) return [part[len(prefix):] for part in sans_parts if part.startswith(prefix)] def _pyopenssl_extract_san_list_raw(cert_or_req: Union[crypto.X509, crypto.X509Req]) -> List[str]: """Get raw SAN string from cert or csr, parse it as UTF-8 and return. :param cert_or_req: Certificate or CSR. :type cert_or_req: `OpenSSL.crypto.X509` or `OpenSSL.crypto.X509Req`. :returns: raw san strings, parsed byte as utf-8 :rtype: `list` of `unicode` """ # This function finds SANs by dumping the certificate/CSR to text and # searching for "X509v3 Subject Alternative Name" in the text. This method # is used to because in PyOpenSSL version <0.17 `_subjectAltNameString` methods are # not able to Parse IP Addresses in subjectAltName string. if isinstance(cert_or_req, crypto.X509): # pylint: disable=line-too-long text = crypto.dump_certificate(crypto.FILETYPE_TEXT, cert_or_req).decode('utf-8') else: text = crypto.dump_certificate_request(crypto.FILETYPE_TEXT, cert_or_req).decode('utf-8') # WARNING: this function does not support multiple SANs extensions. # Multiple X509v3 extensions of the same type is disallowed by RFC 5280. raw_san = re.search(r"X509v3 Subject Alternative Name:(?: critical)?\s*(.*)", text) parts_separator = ", " # WARNING: this function assumes that no SAN can include # parts_separator, hence the split! sans_parts = [] if raw_san is None else raw_san.group(1).split(parts_separator) return sans_parts def gen_ss_cert(key: crypto.PKey, domains: Optional[List[str]] = None, not_before: Optional[int] = None, validity: int = (7 * 24 * 60 * 60), force_san: bool = True, extensions: Optional[List[crypto.X509Extension]] = None, ips: Optional[List[Union[ipaddress.IPv4Address, ipaddress.IPv4Address]]] = None ) -> crypto.X509: """Generate new self-signed certificate. :type domains: `list` of `unicode` :param OpenSSL.crypto.PKey key: :param bool force_san: :param extensions: List of additional extensions to include in the cert. :type extensions: `list` of `OpenSSL.crypto.X509Extension` :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." cert = crypto.X509() cert.set_serial_number(int(binascii.hexlify(os.urandom(16)), 16)) cert.set_version(2) if extensions is None: extensions = [] if domains is None: domains = [] if ips is None: ips = [] extensions.append( crypto.X509Extension( b"basicConstraints", True, b"CA:TRUE, pathlen:0"), ) if len(domains) > 0: cert.get_subject().CN = domains[0] # TODO: what to put into cert.get_subject()? cert.set_issuer(cert.get_subject()) sanlist = [] for address in domains: sanlist.append('DNS:' + address) for ip in ips: sanlist.append('IP:' + ip.exploded) san_string = ', '.join(sanlist).encode('ascii') if force_san or len(domains) > 1 or len(ips) > 0: extensions.append(crypto.X509Extension( b"subjectAltName", critical=False, value=san_string )) cert.add_extensions(extensions) cert.gmtime_adj_notBefore(0 if not_before is None else not_before) cert.gmtime_adj_notAfter(validity) cert.set_pubkey(key) cert.sign(key, "sha256") return cert def dump_pyopenssl_chain(chain: List[crypto.X509], filetype: int = crypto.FILETYPE_PEM) -> bytes: """Dump certificate chain into a bundle. :param list chain: List of `OpenSSL.crypto.X509` (or wrapped in :class:`josepy.util.ComparableX509`). :returns: certificate chain bundle :rtype: bytes """ # XXX: returns empty string when no chain is available, which # shuts up RenewableCert, but might not be the best solution... def _dump_cert(cert: Union[jose.ComparableX509, crypto.X509]) -> bytes: if isinstance(cert, jose.ComparableX509): cert = cert.wrapped return crypto.dump_certificate(filetype, cert) # assumes that OpenSSL.crypto.dump_certificate includes ending # newline character return b"".join(_dump_cert(cert) for cert in chain) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1635888438.0 acme-1.21.0/acme/errors.py0000644000076500000240000001033300000000000013744 0ustar00bmwstaff"""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=1635888438.0 acme-1.21.0/acme/fields.py0000644000076500000240000000353700000000000013706 0ustar00bmwstaff"""ACME JSON fields.""" import datetime from typing import Any import logging 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) class Resource(jose.Field): """Resource MITM field.""" def __init__(self, resource_type: str, *args: Any, **kwargs: Any) -> None: self.resource_type = resource_type super().__init__( 'resource', default=resource_type, *args, **kwargs) def decode(self, value: Any) -> Any: if value != self.resource_type: raise jose.DeserializationError( 'Wrong resource type: {0} instead of {1}'.format( value, self.resource_type)) return value ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1635888438.0 acme-1.21.0/acme/jws.py0000644000076500000240000000452300000000000013237 0ustar00bmwstaff"""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 = jose.Field('nonce', omitempty=True, encoder=jose.encode_b64jose) kid = jose.Field('kid', omitempty=True) url = 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 def nonce(value: str) -> bytes: # 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=no-member # TODO: decoder/encoder should accept cls? Otherwise, subclassing # JSONObjectWithFields is tricky... header_cls = 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 @classmethod # 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=1635888438.0 acme-1.21.0/acme/magic_typing.py0000644000076500000240000000112300000000000015077 0ustar00bmwstaff"""Simple shim around the typing module. This was useful when this code supported Python 2 and typing wasn't always available. This code is being kept for now for backwards compatibility. """ import warnings from typing import * # pylint: disable=wildcard-import, unused-wildcard-import from typing import Any warnings.warn("acme.magic_typing is deprecated and will be removed in a future release.", DeprecationWarning) class TypingClass: """Ignore import errors by getting anything""" def __getattr__(self, name: str) -> Any: return None # pragma: no cover ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1635888438.0 acme-1.21.0/acme/messages.py0000644000076500000240000006137500000000000014253 0ustar00bmwstaff"""ACME protocol messages.""" from collections.abc import Hashable 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 Tuple from typing import Type from typing import Optional import josepy as jose from acme import challenges from acme import errors from acme import fields from acme import jws from acme import util from acme.mixins import ResourceMixin OLD_ERROR_PREFIX = "urn:acme:error:" 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 = dict( (ERROR_PREFIX + name, desc) for name, desc in ERROR_CODES.items()) ERROR_TYPE_DESCRIPTIONS.update(dict( # add errors with old prefix, deprecate me (OLD_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) or (OLD_ERROR_PREFIX in err.typ) return False class Error(jose.JSONObjectWithFields, errors.Error): """ACME error. https://tools.ietf.org/html/draft-ietf-appsawg-http-problem-00 :ivar unicode typ: :ivar unicode title: :ivar unicode detail: """ typ = jose.Field('type', omitempty=True, default='about:blank') title = jose.Field('title', omitempty=True) detail = jose.Field('detail', omitempty=True) @classmethod def with_code(cls, code: str, **kwargs: Any) -> 'Error': """Create an Error instance with an ACME Error code. :unicode 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) # type: ignore @property def description(self) -> Optional[str]: """Hardcoded error description based on its type. :returns: Description if standard ACME error or ``None``. :rtype: unicode """ 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: unicode """ code = str(self.typ).rsplit(':', maxsplit=1)[-1] if code in ERROR_CODES: return code return None def __str__(self) -> str: return b' :: '.join( part.encode('ascii', 'backslashreplace') for part in (self.typ, self.description, self.detail, self.title) if part is not None).decode() 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( '{0} not recognized'.format(cls.__name__)) return cls.POSSIBLE_NAMES[jobj] def __repr__(self) -> str: return '{0}({1})'.format(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 Status(_Constant): """ACME "status" field.""" POSSIBLE_NAMES: Dict[str, 'Status'] = {} 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 IdentifierType(_Constant): """ACME identifier type.""" POSSIBLE_NAMES: Dict[str, 'IdentifierType'] = {} 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 unicode value: """ typ = jose.Field('type', decoder=IdentifierType.from_json) value = jose.Field('value') class Directory(jose.JSONDeSerializable): """Directory.""" _REGISTERED_TYPES: Dict[str, Type['Directory']] = {} class Meta(jose.JSONObjectWithFields): """Directory Meta.""" _terms_of_service = jose.Field('terms-of-service', omitempty=True) _terms_of_service_v2 = jose.Field('termsOfService', omitempty=True) website = jose.Field('website', omitempty=True) caa_identities = jose.Field('caaIdentities', omitempty=True) external_account_required = jose.Field('externalAccountRequired', 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 or self._terms_of_service_v2 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 @classmethod def _canon_key(cls, key: str) -> str: return getattr(key, 'resource_type', key) @classmethod def register(cls, resource_body_cls: Type['Directory']) -> Type['Directory']: """Register resource.""" resource_type = resource_body_cls.resource_type assert resource_type not in cls._REGISTERED_TYPES cls._REGISTERED_TYPES[resource_type] = resource_body_cls return resource_body_cls def __init__(self, jobj: Mapping[str, Any]) -> None: canon_jobj = util.map_keys(jobj, self._canon_key) # TODO: check that everything is an absolute URL; acme-spec is # not clear on that self._jobj = canon_jobj def __getattr__(self, name: str) -> Any: try: return self[name.replace('_', '-')] except KeyError as error: raise AttributeError(str(error)) def __getitem__(self, name: str) -> Any: try: return self._jobj[self._canon_key(name)] except KeyError: raise KeyError('Directory field "' + self._canon_key(name) + '" not found') def to_partial_json(self) -> Dict[str, Any]: return self._jobj @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 = jose.Field('body') class ResourceWithURI(Resource): """ACME Resource with URI. :ivar unicode ~.uri: Location of the resource. """ uri = 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() class Registration(ResourceBody): """Registration Resource Body. :ivar josepy.jwk.JWK key: Public key. :ivar tuple contact: Contact information following ACME spec, `tuple` of `unicode`. :ivar unicode agreement: """ # on new-reg key server ignores 'key' and populates it based on # JWS.signature.combined.jwk key = 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 = jose.Field('contact', omitempty=True, default=()) agreement = jose.Field('agreement', omitempty=True) status = jose.Field('status', omitempty=True) terms_of_service_agreed = jose.Field('termsOfServiceAgreed', omitempty=True) only_return_existing = jose.Field('onlyReturnExisting', omitempty=True) external_account_binding = jose.Field('externalAccountBinding', omitempty=True) phone_prefix = 'tel:' email_prefix = 'mailto:' @classmethod def from_data(cls, phone: Optional[str] = None, email: Optional[str] = None, external_account_binding: Optional[ExternalAccountBinding] = None, **kwargs: Any) -> 'Registration': """ 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) @Directory.register class NewRegistration(ResourceMixin, Registration): """New registration.""" resource_type = 'new-reg' resource = fields.Resource(resource_type) class UpdateRegistration(ResourceMixin, Registration): """Update registration.""" resource_type = 'reg' resource = fields.Resource(resource_type) class RegistrationResource(ResourceWithURI): """Registration Resource. :ivar acme.messages.Registration body: :ivar unicode new_authzr_uri: Deprecated. Do not use. :ivar unicode terms_of_service: URL for the CA TOS. """ body = jose.Field('body', decoder=Registration.from_json) new_authzr_uri = jose.Field('new_authzr_uri', omitempty=True) terms_of_service = 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. _uri = jose.Field('uri', omitempty=True, default=None) _url = jose.Field('url', omitempty=True, default=None) status = jose.Field('status', decoder=Status.from_json, omitempty=True, default=STATUS_PENDING) validated = fields.RFC3339Field('validated', omitempty=True) 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 or self._uri 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 name[1:] if name == '_uri' else name def _internal_name(self, name: str) -> str: return '_' + name if name == 'uri' else name class ChallengeResource(Resource): """Challenge Resource. :ivar acme.messages.ChallengeBody body: :ivar unicode authzr_uri: URI found in the 'up' ``Link`` header. """ body = jose.Field('body', decoder=ChallengeBody.from_json) authzr_uri = jose.Field('authzr_uri') @property def uri(self) -> str: """The URL of the challenge body.""" return self.body.uri class Authorization(ResourceBody): """Authorization Resource Body. :ivar acme.messages.Identifier identifier: :ivar list challenges: `list` of `.ChallengeBody` :ivar tuple combinations: Challenge combinations (`tuple` of `tuple` of `int`, as opposed to `list` of `list` from the spec). :ivar acme.messages.Status status: :ivar datetime.datetime expires: """ identifier = jose.Field('identifier', decoder=Identifier.from_json, omitempty=True) challenges = jose.Field('challenges', omitempty=True) combinations = jose.Field('combinations', omitempty=True) 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 = fields.RFC3339Field('expires', omitempty=True) wildcard = 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[Mapping[str, Any]]) -> Tuple[ChallengeBody, ...]: # pylint: disable=no-self-argument,missing-function-docstring return tuple(ChallengeBody.from_json(chall) for chall in value) @property def resolved_combinations(self) -> Tuple[Tuple[Dict[str, Any], ...], ...]: """Combinations with challenges instead of indices.""" return tuple(tuple(self.challenges[idx] for idx in combo) for combo in self.combinations) # pylint: disable=not-an-iterable @Directory.register class NewAuthorization(ResourceMixin, Authorization): """New authorization.""" resource_type = 'new-authz' resource = fields.Resource(resource_type) class UpdateAuthorization(ResourceMixin, Authorization): """Update authorization.""" resource_type = 'authz' resource = fields.Resource(resource_type) class AuthorizationResource(ResourceWithURI): """Authorization Resource. :ivar acme.messages.Authorization body: :ivar unicode new_cert_uri: Deprecated. Do not use. """ body = jose.Field('body', decoder=Authorization.from_json) new_cert_uri = jose.Field('new_cert_uri', omitempty=True) @Directory.register class CertificateRequest(ResourceMixin, jose.JSONObjectWithFields): """ACME new-cert request. :ivar josepy.util.ComparableX509 csr: `OpenSSL.crypto.X509Req` wrapped in `.ComparableX509` """ resource_type = 'new-cert' resource = fields.Resource(resource_type) csr = jose.Field('csr', decoder=jose.decode_csr, encoder=jose.encode_csr) class CertificateResource(ResourceWithURI): """Certificate Resource. :ivar josepy.util.ComparableX509 body: `OpenSSL.crypto.X509` wrapped in `.ComparableX509` :ivar unicode cert_chain_uri: URI found in the 'up' ``Link`` header :ivar tuple authzrs: `tuple` of `AuthorizationResource`. """ cert_chain_uri = jose.Field('cert_chain_uri') authzrs = jose.Field('authzrs') @Directory.register class Revocation(ResourceMixin, jose.JSONObjectWithFields): """Revocation message. :ivar .ComparableX509 certificate: `OpenSSL.crypto.X509` wrapped in `.ComparableX509` """ resource_type = 'revoke-cert' resource = fields.Resource(resource_type) certificate = jose.Field( 'certificate', decoder=jose.decode_cert, encoder=jose.encode_cert) reason = jose.Field('reason') class Order(ResourceBody): """Order Resource Body. :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. """ identifiers = jose.Field('identifiers', omitempty=True) status = jose.Field('status', decoder=Status.from_json, omitempty=True) authorizations = jose.Field('authorizations', omitempty=True) certificate = jose.Field('certificate', omitempty=True) finalize = jose.Field('finalize', omitempty=True) expires = fields.RFC3339Field('expires', omitempty=True) 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[Mapping[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 str 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 = jose.Field('body', decoder=Order.from_json) csr_pem = jose.Field('csr_pem', omitempty=True) authorizations = jose.Field('authorizations') fullchain_pem = jose.Field('fullchain_pem', omitempty=True) alternative_fullchains_pem = jose.Field('alternative_fullchains_pem', omitempty=True) @Directory.register class NewOrder(Order): """New order.""" resource_type = 'new-order' ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1635888438.0 acme-1.21.0/acme/mixins.py0000644000076500000240000000543400000000000013745 0ustar00bmwstaff"""Useful mixins for Challenge and Resource objects""" from typing import Any from typing import Dict class VersionedLEACMEMixin: """This mixin stores the version of Let's Encrypt's endpoint being used.""" @property def le_acme_version(self) -> int: """Define the version of ACME protocol to use""" return getattr(self, '_le_acme_version', 1) @le_acme_version.setter def le_acme_version(self, version: int) -> None: # We need to use object.__setattr__ to not depend on the specific implementation of # __setattr__ in current class (eg. jose.TypedJSONObjectWithFields raises AttributeError # for any attempt to set an attribute to make objects immutable). object.__setattr__(self, '_le_acme_version', version) def __setattr__(self, key: str, value: Any) -> None: if key == 'le_acme_version': # Required for @property to operate properly. See comment above. object.__setattr__(self, key, value) else: super().__setattr__(key, value) # pragma: no cover class ResourceMixin(VersionedLEACMEMixin): """ This mixin generates a RFC8555 compliant JWS payload by removing the `resource` field if needed (eg. ACME v2 protocol). """ def to_partial_json(self) -> Dict[str, Any]: """See josepy.JSONDeserializable.to_partial_json()""" return _safe_jobj_compliance(super(), 'to_partial_json', 'resource') def fields_to_partial_json(self) -> Dict[str, Any]: """See josepy.JSONObjectWithFields.fields_to_partial_json()""" return _safe_jobj_compliance(super(), 'fields_to_partial_json', 'resource') class TypeMixin(VersionedLEACMEMixin): """ This mixin allows generation of a RFC8555 compliant JWS payload by removing the `type` field if needed (eg. ACME v2 protocol). """ def to_partial_json(self) -> Dict[str, Any]: """See josepy.JSONDeserializable.to_partial_json()""" return _safe_jobj_compliance(super(), 'to_partial_json', 'type') def fields_to_partial_json(self) -> Dict[str, Any]: """See josepy.JSONObjectWithFields.fields_to_partial_json()""" return _safe_jobj_compliance(super(), 'fields_to_partial_json', 'type') def _safe_jobj_compliance(instance: Any, jobj_method: str, uncompliant_field: str) -> Dict[str, Any]: if hasattr(instance, jobj_method): jobj: Dict[str, Any] = getattr(instance, jobj_method)() if instance.le_acme_version == 2: jobj.pop(uncompliant_field, None) return jobj raise AttributeError('Method {0}() is not implemented.'.format(jobj_method)) # pragma: no cover ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1635888438.0 acme-1.21.0/acme/standalone.py0000644000076500000240000003152600000000000014567 0ustar00bmwstaff"""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 List from typing import Mapping from typing import Optional from typing import Set from typing import Tuple from typing import Type from OpenSSL import crypto 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) socketserver.TCPServer.__init__(self, *args, **kwargs) def _wrap_sock(self) -> None: self.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 ) -> Tuple[crypto.PKey, crypto.X509]: # pragma: no cover """Callback selecting certificate for connection.""" server_name = connection.get_servername() return self.certs.get(server_name, 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 socket.error 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 socket.error("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[Tuple[crypto.PKey, crypto.X509]], challenge_certs: Mapping[str, Tuple[crypto.PKey, crypto.X509]], ipv6: bool = False) -> None: TLSServer.__init__( self, server_address, _BaseRequestHandlerWithLogging, certs=certs, ipv6=ipv6) self.challenge_certs = challenge_certs def _cert_selection(self, connection: SSL.Connection) -> Tuple[crypto.PKey, crypto.X509]: # 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() logger.debug("Serving challenge cert for server name %s", server_name) return self.challenge_certs[server_name] 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 BaseHTTPServer.HTTPServer.__init__(self, *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: HTTPServer.__init__( self, 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: BaseDualNetworkedServers.__init__(self, 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) BaseHTTPServer.BaseHTTPRequestHandler.__init__(self, *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) class _BaseRequestHandlerWithLogging(socketserver.BaseRequestHandler): """BaseRequestHandler with logging.""" 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") socketserver.BaseRequestHandler.handle(self) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1635888438.0 acme-1.21.0/acme/util.py0000644000076500000240000000045700000000000013413 0ustar00bmwstaff"""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=1635888462.3029366 acme-1.21.0/acme.egg-info/0000755000076500000240000000000000000000000013550 5ustar00bmwstaff././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1635888462.0 acme-1.21.0/acme.egg-info/PKG-INFO0000644000076500000240000000156200000000000014651 0ustar00bmwstaffMetadata-Version: 2.1 Name: acme Version: 1.21.0 Summary: ACME protocol implementation in Python Home-page: https://github.com/letsencrypt/letsencrypt Author: Certbot Project Author-email: certbot-dev@eff.org License: Apache License 2.0 Platform: UNKNOWN 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.6 Classifier: Programming Language :: Python :: 3.7 Classifier: Programming Language :: Python :: 3.8 Classifier: Programming Language :: Python :: 3.9 Classifier: Topic :: Internet :: WWW/HTTP Classifier: Topic :: Security Requires-Python: >=3.6 Provides-Extra: docs Provides-Extra: test License-File: LICENSE.txt UNKNOWN ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1635888462.0 acme-1.21.0/acme.egg-info/SOURCES.txt0000644000076500000240000000350600000000000015440 0ustar00bmwstaffLICENSE.txt MANIFEST.in README.rst pytest.ini setup.cfg 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/magic_typing.py acme/messages.py acme/mixins.py 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 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/errors.rst docs/api/fields.rst docs/api/jose.rst docs/api/messages.rst docs/api/standalone.rst docs/man/jws.rst examples/http01_example.py tests/challenges_test.py tests/client_test.py tests/crypto_util_test.py tests/errors_test.py tests/fields_test.py tests/jose_test.py tests/jws_test.py tests/magic_typing_test.py tests/messages_test.py tests/standalone_test.py tests/test_util.py tests/util_test.py tests/testdata/README tests/testdata/cert-100sans.pem tests/testdata/cert-idnsans.pem tests/testdata/cert-ipsans.pem tests/testdata/cert-ipv6sans.pem tests/testdata/cert-nocn.der tests/testdata/cert-san.pem tests/testdata/cert.der tests/testdata/cert.pem tests/testdata/critical-san.pem tests/testdata/csr-100sans.pem tests/testdata/csr-6sans.pem tests/testdata/csr-idnsans.pem tests/testdata/csr-ipsans.pem tests/testdata/csr-ipv6sans.pem tests/testdata/csr-mixed.pem tests/testdata/csr-nosans.pem tests/testdata/csr-san.pem tests/testdata/csr.der tests/testdata/csr.pem tests/testdata/dsa512_key.pem tests/testdata/rsa1024_cert.pem tests/testdata/rsa1024_key.pem tests/testdata/rsa2048_cert.pem tests/testdata/rsa2048_key.pem tests/testdata/rsa256_key.pem tests/testdata/rsa4096_cert.pem tests/testdata/rsa4096_key.pem tests/testdata/rsa512_key.pem././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1635888462.0 acme-1.21.0/acme.egg-info/dependency_links.txt0000644000076500000240000000000100000000000017616 0ustar00bmwstaff ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1635888462.0 acme-1.21.0/acme.egg-info/requires.txt0000644000076500000240000000030100000000000016142 0ustar00bmwstaffcryptography>=2.1.4 josepy>=1.9.0 PyOpenSSL>=17.3.0 pyrfc3339 pytz requests>=2.14.2 requests-toolbelt>=0.3.0 setuptools>=39.0.1 [docs] Sphinx>=1.0 sphinx_rtd_theme [test] pytest pytest-xdist ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1635888462.0 acme-1.21.0/acme.egg-info/top_level.txt0000644000076500000240000000000500000000000016275 0ustar00bmwstaffacme ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1635888462.3036668 acme-1.21.0/docs/0000755000076500000240000000000000000000000012101 5ustar00bmwstaff././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1635888438.0 acme-1.21.0/docs/.gitignore0000644000076500000240000000001100000000000014061 0ustar00bmwstaff/_build/ ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1635888438.0 acme-1.21.0/docs/Makefile0000644000076500000240000001641200000000000013545 0ustar00bmwstaff# 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." ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1635888462.3037763 acme-1.21.0/docs/_static/0000755000076500000240000000000000000000000013527 5ustar00bmwstaff././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1635888438.0 acme-1.21.0/docs/_static/.gitignore0000644000076500000240000000000000000000000015505 0ustar00bmwstaff././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1635888462.3038592 acme-1.21.0/docs/_templates/0000755000076500000240000000000000000000000014236 5ustar00bmwstaff././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1635888438.0 acme-1.21.0/docs/_templates/.gitignore0000644000076500000240000000000000000000000016214 0ustar00bmwstaff././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1635888462.304543 acme-1.21.0/docs/api/0000755000076500000240000000000000000000000012652 5ustar00bmwstaff././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1635888438.0 acme-1.21.0/docs/api/challenges.rst0000644000076500000240000000010400000000000015504 0ustar00bmwstaffChallenges ---------- .. automodule:: acme.challenges :members: ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1635888438.0 acme-1.21.0/docs/api/client.rst0000644000076500000240000000007000000000000014657 0ustar00bmwstaffClient ------ .. automodule:: acme.client :members: ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1635888438.0 acme-1.21.0/docs/api/errors.rst0000644000076500000240000000007000000000000014715 0ustar00bmwstaffErrors ------ .. automodule:: acme.errors :members: ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1635888438.0 acme-1.21.0/docs/api/fields.rst0000644000076500000240000000007000000000000014647 0ustar00bmwstaffFields ------ .. automodule:: acme.fields :members: ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1635888438.0 acme-1.21.0/docs/api/jose.rst0000644000076500000240000000024100000000000014341 0ustar00bmwstaffJOSE ---- 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=1635888438.0 acme-1.21.0/docs/api/messages.rst0000644000076500000240000000007600000000000015216 0ustar00bmwstaffMessages -------- .. automodule:: acme.messages :members: ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1635888438.0 acme-1.21.0/docs/api/standalone.rst0000644000076500000240000000010400000000000015527 0ustar00bmwstaffStandalone ---------- .. automodule:: acme.standalone :members: ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1635888438.0 acme-1.21.0/docs/api.rst0000644000076500000240000000013000000000000013376 0ustar00bmwstaff================= API Documentation ================= .. toctree:: :glob: api/* ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1635888438.0 acme-1.21.0/docs/conf.py0000644000076500000240000002376000000000000013410 0ustar00bmwstaff# -*- 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', ] 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. # https://docs.readthedocs.io/en/stable/faq.html#i-want-to-use-the-read-the-docs-theme-locally # on_rtd is whether we are on readthedocs.org on_rtd = os.environ.get('READTHEDOCS', None) == 'True' if not on_rtd: # only import and set the theme if we're building docs locally import sphinx_rtd_theme html_theme = 'sphinx_rtd_theme' html_theme_path = [sphinx_rtd_theme.get_html_theme_path()] # otherwise, readthedocs.org uses their theme by default, so no need to specify it # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the # documentation. #html_theme_options = {} # Add any paths that contain custom themes here, relative to this directory. #html_theme_path = [] # The name for this set of Sphinx documents. If None, it defaults to # " v documentation". #html_title = None # A shorter title for the navigation bar. Default is the same as html_title. #html_short_title = None # The name of an image file (relative to this directory) to place at the top # of the sidebar. #html_logo = None # The name of an image file (within the static path) to use as favicon of the # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 # pixels large. #html_favicon = None # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". html_static_path = ['_static'] # Add any extra paths that contain custom files (such as robots.txt or # .htaccess) here, relative to this directory. These files are copied # directly to the root of the documentation. #html_extra_path = [] # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, # using the given strftime format. #html_last_updated_fmt = '%b %d, %Y' # If true, SmartyPants will be used to convert quotes and dashes to # typographically correct entities. #html_use_smartypants = True # Custom sidebar templates, maps document names to template names. #html_sidebars = {} # Additional templates that should be rendered to pages, maps page names to # template names. #html_additional_pages = {} # If false, no module index is generated. #html_domain_indices = True # If false, no index is generated. #html_use_index = True # If true, the index is split into individual pages for each letter. #html_split_index = False # If true, links to the reST sources are added to the pages. #html_show_sourcelink = True # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. #html_show_sphinx = True # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. #html_show_copyright = True # If true, an OpenSearch description file will be output, and all pages will # contain a tag referring to it. The value of this option must be the # base URL from which the finished HTML is served. #html_use_opensearch = '' # This is the file name suffix for HTML files (e.g. ".xhtml"). #html_file_suffix = None # Language to be used for generating the HTML full-text search index. # Sphinx supports the following languages: # 'da', 'de', 'en', 'es', 'fi', 'fr', 'hu', 'it', 'ja' # 'nl', 'no', 'pt', 'ro', 'ru', 'sv', 'tr' #html_search_language = 'en' # A dictionary with options for the search language support, empty by default. # Now only 'ja' uses this config value #html_search_options = {'type': 'default'} # The name of a javascript file (relative to the configuration directory) that # implements a search results scorer. If empty, the default will be used. #html_search_scorer = 'scorer.js' # Output file base name for HTML help builder. htmlhelp_basename = '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=1635888438.0 acme-1.21.0/docs/index.rst0000644000076500000240000000073700000000000013751 0ustar00bmwstaff.. 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=1635888438.0 acme-1.21.0/docs/jws-help.txt0000644000076500000240000000024700000000000014376 0ustar00bmwstaffusage: jws [-h] [--compact] {sign,verify} ... positional arguments: {sign,verify} optional arguments: -h, --help show this help message and exit --compact ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1635888438.0 acme-1.21.0/docs/make.bat0000644000076500000240000001613300000000000013512 0ustar00bmwstaff@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 ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1635888462.304641 acme-1.21.0/docs/man/0000755000076500000240000000000000000000000012654 5ustar00bmwstaff././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1635888438.0 acme-1.21.0/docs/man/jws.rst0000644000076500000240000000005600000000000014212 0ustar00bmwstaff:orphan: .. literalinclude:: ../jws-help.txt ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1635888462.3047462 acme-1.21.0/examples/0000755000076500000240000000000000000000000012767 5ustar00bmwstaff././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1635888438.0 acme-1.21.0/examples/http01_example.py0000644000076500000240000001605000000000000016176 0ustar00bmwstaff"""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.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 = OpenSSL.crypto.PKey() pkey.generate_key(OpenSSL.crypto.TYPE_RSA, CERT_PKEY_BITS) pkey_pem = OpenSSL.crypto.dump_privatekey(OpenSSL.crypto.FILETYPE_PEM, pkey) 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 = messages.Directory.from_json(net.get(DIRECTORY_URL).json()) 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 = jose.ComparableX509( OpenSSL.crypto.load_certificate( OpenSSL.crypto.FILETYPE_PEM, 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.OLD_ERROR_PREFIX + 'unauthorized' \ or 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=1635888438.0 acme-1.21.0/pytest.ini0000644000076500000240000000007700000000000013206 0ustar00bmwstaff[pytest] norecursedirs = .* build dist CVS _darcs {arch} *.egg ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1635888462.309311 acme-1.21.0/setup.cfg0000644000076500000240000000010300000000000012764 0ustar00bmwstaff[bdist_wheel] universal = 1 [egg_info] tag_build = tag_date = 0 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1635888439.0 acme-1.21.0/setup.py0000644000076500000240000000273500000000000012672 0ustar00bmwstaffimport sys from setuptools import find_packages from setuptools import setup version = '1.21.0' install_requires = [ 'cryptography>=2.1.4', 'josepy>=1.9.0', 'PyOpenSSL>=17.3.0', 'pyrfc3339', 'pytz', 'requests>=2.14.2', 'requests-toolbelt>=0.3.0', 'setuptools>=39.0.1', ] docs_extras = [ 'Sphinx>=1.0', # autodoc_member_order = 'bysource', autodoc_default_flags 'sphinx_rtd_theme', ] test_extras = [ 'pytest', 'pytest-xdist', ] setup( name='acme', version=version, description='ACME protocol implementation in Python', url='https://github.com/letsencrypt/letsencrypt', author="Certbot Project", author_email='certbot-dev@eff.org', license='Apache License 2.0', python_requires='>=3.6', 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.6', 'Programming Language :: Python :: 3.7', 'Programming Language :: Python :: 3.8', 'Programming Language :: Python :: 3.9', '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, }, ) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1635888462.3060584 acme-1.21.0/tests/0000755000076500000240000000000000000000000012313 5ustar00bmwstaff././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1635888438.0 acme-1.21.0/tests/challenges_test.py0000644000076500000240000004416700000000000016045 0ustar00bmwstaff"""Tests for acme.challenges.""" import urllib.parse as urllib_parse import unittest from unittest import mock import josepy as jose import OpenSSL import requests from acme import errors import test_util CERT = test_util.load_comparable_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"}) self.assertEqual(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): self.assertEqual(self.jobj, self.chall.to_partial_json()) def test_from_json(self): from acme.challenges import UnrecognizedChallenge self.assertEqual( 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') self.assertTrue(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') self.assertFalse(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') self.assertFalse(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') self.assertFalse(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): self.assertEqual({k: v for k, v in self.jmsg.items() if k != 'keyAuthorization'}, self.msg.to_partial_json()) def test_from_json(self): from acme.challenges import DNS01Response self.assertEqual(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) self.assertFalse(verified) def test_simple_verify_success(self): public_key = KEY.public_key() verified = self.response.simple_verify(self.chall, "local", public_key) self.assertTrue(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): self.assertEqual('_acme-challenge.www.example.com', self.msg.validation_domain_name('www.example.com')) def test_validation(self): self.assertEqual( "rAa7iIg4K2y63fvUhCfy8dP1Xl7wEhmQq0oChTcE3Zk", self.msg.validation(KEY)) def test_to_partial_json(self): self.assertEqual(self.jmsg, self.msg.to_partial_json()) def test_from_json(self): from acme.challenges import DNS01 self.assertEqual(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): self.assertEqual({k: v for k, v in self.jmsg.items() if k != 'keyAuthorization'}, self.msg.to_partial_json()) def test_from_json(self): from acme.challenges import HTTP01Response self.assertEqual( 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) self.assertTrue(self.response.simple_verify( self.chall, "local", KEY.public_key())) mock_get.assert_called_once_with(self.chall.uri("local"), verify=False) @mock.patch("acme.challenges.requests.get") def test_simple_verify_bad_validation(self, mock_get): mock_get.return_value = mock.MagicMock(text="!") self.assertFalse(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)) self.assertTrue(self.response.simple_verify( self.chall, "local", KEY.public_key())) mock_get.assert_called_once_with(self.chall.uri("local"), verify=False) @mock.patch("acme.challenges.requests.get") def test_simple_verify_connection_error(self, mock_get): mock_get.side_effect = requests.exceptions.RequestException self.assertFalse(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) self.assertEqual("local:8080", urllib_parse.urlparse( mock_get.mock_calls[0][1][0]).netloc) 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): self.assertEqual(self.msg.path, '/.well-known/acme-challenge/' 'evaGxfADs6pSRb2LAv9IZf17Dt3juxGJ-PCt92wr-oA') def test_uri(self): self.assertEqual( 'http://example.com/.well-known/acme-challenge/' 'evaGxfADs6pSRb2LAv9IZf17Dt3juxGJ-PCt92wr-oA', self.msg.uri('example.com')) def test_to_partial_json(self): self.assertEqual(self.jmsg, self.msg.to_partial_json()) def test_from_json(self): from acme.challenges import HTTP01 self.assertEqual(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): self.assertTrue(self.msg.good_token) self.assertFalse( 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): self.assertEqual({k: v for k, v in self.jmsg.items() if k != 'keyAuthorization'}, self.response.to_partial_json()) def test_from_json(self): from acme.challenges import TLSALPN01Response self.assertEqual(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) self.assertEqual(key1, key2) self.assertTrue(self.response.verify_cert(self.domain, cert)) def test_gen_verify_cert_gen_key(self): cert, key = self.response.gen_cert(self.domain) self.assertIsInstance(key, OpenSSL.crypto.PKey) self.assertTrue(self.response.verify_cert(self.domain, cert)) def test_verify_bad_cert(self): self.assertFalse(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) self.assertEqual(key1, key2) self.assertFalse(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 self.assertEqual( 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=['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=['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 self.assertFalse(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): self.assertEqual(self.jmsg, self.msg.to_partial_json()) def test_from_json(self): from acme.challenges import TLSALPN01 self.assertEqual(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') self.assertRaises( 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') self.assertEqual(('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): self.assertEqual(self.jmsg, self.msg.to_partial_json()) def test_from_json(self): from acme.challenges import DNS self.assertEqual(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): self.assertTrue(self.msg.check_validation( self.msg.gen_validation(KEY), KEY.public_key())) def test_gen_check_validation_wrong_key(self): key2 = jose.JWKRSA.load(test_util.load_vector('rsa1024_key.pem')) self.assertFalse(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: self.assertFalse(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) self.assertFalse(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 self.assertIsInstance(response, DNSResponse) self.assertEqual(response.validation, mock.sentinel.validation) def test_validation_domain_name(self): self.assertEqual( '_acme-challenge.le.wtf', self.msg.validation_domain_name('le.wtf')) 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 = { 'resource': 'challenge', 'type': 'dns', 'validation': self.validation, } self.jmsg_from = { 'resource': 'challenge', 'type': 'dns', 'validation': self.validation.to_json(), } def test_to_partial_json(self): self.assertEqual(self.jmsg_to, self.msg.to_partial_json()) def test_from_json(self): from acme.challenges import DNSResponse self.assertEqual(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): self.assertTrue( 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() challenge_body.le_acme_version = 2 jobj = challenge_body.json_dumps(indent=2).encode() # RFC8555 states that challenge responses must have an empty payload. self.assertEqual(jobj, b'{}') if __name__ == '__main__': unittest.main() # pragma: no cover ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1635888438.0 acme-1.21.0/tests/client_test.py0000644000076500000240000016330400000000000015211 0ustar00bmwstaff"""Tests for acme.client.""" # pylint: disable=too-many-lines import copy import datetime import http.client as http_client import ipaddress import json import unittest from typing import Dict from unittest import mock import josepy as jose import OpenSSL import requests from acme import challenges from acme import errors from acme import jws as acme_jws from acme import messages from acme.mixins import VersionedLEACMEMixin import messages_test import test_util CERT_DER = test_util.load_vector('cert.der') CERT_SAN_PEM = test_util.load_vector('cert-san.pem') CSR_SAN_PEM = test_util.load_vector('csr-san.pem') CSR_MIXED_PEM = test_util.load_vector('csr-mixed.pem') KEY = jose.JWKRSA.load(test_util.load_vector('rsa512_key.pem')) KEY2 = jose.JWKRSA.load(test_util.load_vector('rsa256_key.pem')) DIRECTORY_V1 = messages.Directory({ messages.NewRegistration: 'https://www.letsencrypt-demo.org/acme/new-reg', messages.Revocation: 'https://www.letsencrypt-demo.org/acme/revoke-cert', messages.NewAuthorization: 'https://www.letsencrypt-demo.org/acme/new-authz', messages.CertificateRequest: 'https://www.letsencrypt-demo.org/acme/new-cert', }) 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', }) class ClientTestBase(unittest.TestCase): """Base for tests in acme.client.""" 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,), combinations=None) self.authzr = messages.AuthorizationResource( body=self.authz, uri=authzr_uri) # Reason code for revocation self.rsn = 1 class BackwardsCompatibleClientV2Test(ClientTestBase): """Tests for acme.client.BackwardsCompatibleClientV2.""" def setUp(self): super().setUp() # contains a loaded cert self.certr = messages.CertificateResource( body=messages_test.CERT) loaded = OpenSSL.crypto.load_certificate( OpenSSL.crypto.FILETYPE_PEM, CERT_SAN_PEM) wrapped = jose.ComparableX509(loaded) self.chain = [wrapped, wrapped] self.cert_pem = OpenSSL.crypto.dump_certificate( OpenSSL.crypto.FILETYPE_PEM, messages_test.CERT.wrapped).decode() single_chain = OpenSSL.crypto.dump_certificate( OpenSSL.crypto.FILETYPE_PEM, loaded).decode() self.chain_pem = single_chain + single_chain self.fullchain_pem = self.cert_pem + self.chain_pem self.orderr = messages.OrderResource( csr_pem=CSR_SAN_PEM) def _init(self): uri = 'http://www.letsencrypt-demo.org/directory' from acme.client import BackwardsCompatibleClientV2 return BackwardsCompatibleClientV2(net=self.net, key=KEY, server=uri) def test_init_downloads_directory(self): uri = 'http://www.letsencrypt-demo.org/directory' from acme.client import BackwardsCompatibleClientV2 BackwardsCompatibleClientV2(net=self.net, key=KEY, server=uri) self.net.get.assert_called_once_with(uri) def test_init_acme_version(self): self.response.json.return_value = DIRECTORY_V1.to_json() client = self._init() self.assertEqual(client.acme_version, 1) self.response.json.return_value = DIRECTORY_V2.to_json() client = self._init() self.assertEqual(client.acme_version, 2) def test_query_registration_client_v2(self): self.response.json.return_value = DIRECTORY_V2.to_json() client = self._init() self.response.json.return_value = self.regr.body.to_json() self.assertEqual(self.regr, client.query_registration(self.regr)) def test_forwarding(self): self.response.json.return_value = DIRECTORY_V1.to_json() client = self._init() self.assertEqual(client.directory, client.client.directory) self.assertEqual(client.key, KEY) self.assertEqual(client.deactivate_registration, client.client.deactivate_registration) self.assertRaises(AttributeError, client.__getattr__, 'nonexistent') self.assertRaises(AttributeError, client.__getattr__, 'new_account_and_tos') self.assertRaises(AttributeError, client.__getattr__, 'new_account') def test_new_account_and_tos(self): # v2 no tos self.response.json.return_value = DIRECTORY_V2.to_json() with mock.patch('acme.client.ClientV2') as mock_client: client = self._init() client.new_account_and_tos(self.new_reg) mock_client().new_account.assert_called_with(self.new_reg) # v2 tos good with mock.patch('acme.client.ClientV2') as mock_client: mock_client().directory.meta.__contains__.return_value = True client = self._init() client.new_account_and_tos(self.new_reg, lambda x: True) mock_client().new_account.assert_called_with( self.new_reg.update(terms_of_service_agreed=True)) # v2 tos bad with mock.patch('acme.client.ClientV2') as mock_client: mock_client().directory.meta.__contains__.return_value = True client = self._init() def _tos_cb(tos): raise errors.Error self.assertRaises(errors.Error, client.new_account_and_tos, self.new_reg, _tos_cb) mock_client().new_account.assert_not_called() # v1 yes tos self.response.json.return_value = DIRECTORY_V1.to_json() with mock.patch('acme.client.Client') as mock_client: regr = mock.MagicMock(terms_of_service="TOS") mock_client().register.return_value = regr client = self._init() client.new_account_and_tos(self.new_reg) mock_client().register.assert_called_once_with(self.new_reg) mock_client().agree_to_tos.assert_called_once_with(regr) # v1 no tos with mock.patch('acme.client.Client') as mock_client: regr = mock.MagicMock(terms_of_service=None) mock_client().register.return_value = regr client = self._init() client.new_account_and_tos(self.new_reg) mock_client().register.assert_called_once_with(self.new_reg) mock_client().agree_to_tos.assert_not_called() @mock.patch('OpenSSL.crypto.load_certificate_request') @mock.patch('acme.crypto_util._pyopenssl_cert_or_req_all_names') def test_new_order_v1(self, mock__pyopenssl_cert_or_req_all_names, unused_mock_load_certificate_request): self.response.json.return_value = DIRECTORY_V1.to_json() mock__pyopenssl_cert_or_req_all_names.return_value = ['example.com', 'www.example.com'] mock_csr_pem = mock.MagicMock() with mock.patch('acme.client.Client') as mock_client: mock_client().request_domain_challenges.return_value = mock.sentinel.auth client = self._init() orderr = client.new_order(mock_csr_pem) self.assertEqual(orderr.authorizations, [mock.sentinel.auth, mock.sentinel.auth]) def test_new_order_v2(self): self.response.json.return_value = DIRECTORY_V2.to_json() mock_csr_pem = mock.MagicMock() with mock.patch('acme.client.ClientV2') as mock_client: client = self._init() client.new_order(mock_csr_pem) mock_client().new_order.assert_called_once_with(mock_csr_pem) @mock.patch('acme.client.Client') def test_finalize_order_v1_success(self, mock_client): self.response.json.return_value = DIRECTORY_V1.to_json() mock_client().request_issuance.return_value = self.certr mock_client().fetch_chain.return_value = self.chain deadline = datetime.datetime(9999, 9, 9) client = self._init() result = client.finalize_order(self.orderr, deadline) self.assertEqual(result.fullchain_pem, self.fullchain_pem) mock_client().fetch_chain.assert_called_once_with(self.certr) @mock.patch('acme.client.Client') def test_finalize_order_v1_fetch_chain_error(self, mock_client): self.response.json.return_value = DIRECTORY_V1.to_json() mock_client().request_issuance.return_value = self.certr mock_client().fetch_chain.return_value = self.chain mock_client().fetch_chain.side_effect = [errors.Error, self.chain] deadline = datetime.datetime(9999, 9, 9) client = self._init() result = client.finalize_order(self.orderr, deadline) self.assertEqual(result.fullchain_pem, self.fullchain_pem) self.assertEqual(mock_client().fetch_chain.call_count, 2) @mock.patch('acme.client.Client') def test_finalize_order_v1_timeout(self, mock_client): self.response.json.return_value = DIRECTORY_V1.to_json() mock_client().request_issuance.return_value = self.certr deadline = deadline = datetime.datetime.now() - datetime.timedelta(seconds=60) client = self._init() self.assertRaises(errors.TimeoutError, client.finalize_order, self.orderr, deadline) def test_finalize_order_v2(self): self.response.json.return_value = DIRECTORY_V2.to_json() mock_orderr = mock.MagicMock() mock_deadline = mock.MagicMock() with mock.patch('acme.client.ClientV2') as mock_client: client = self._init() client.finalize_order(mock_orderr, mock_deadline) mock_client().finalize_order.assert_called_once_with(mock_orderr, mock_deadline, False) def test_revoke(self): self.response.json.return_value = DIRECTORY_V1.to_json() with mock.patch('acme.client.Client') as mock_client: client = self._init() client.revoke(messages_test.CERT, self.rsn) mock_client().revoke.assert_called_once_with(messages_test.CERT, self.rsn) self.response.json.return_value = DIRECTORY_V2.to_json() with mock.patch('acme.client.ClientV2') as mock_client: client = self._init() client.revoke(messages_test.CERT, self.rsn) mock_client().revoke.assert_called_once_with(messages_test.CERT, self.rsn) def test_update_registration(self): self.response.json.return_value = DIRECTORY_V1.to_json() with mock.patch('acme.client.Client') as mock_client: client = self._init() client.update_registration(mock.sentinel.regr, None) mock_client().update_registration.assert_called_once_with(mock.sentinel.regr, None) # newNonce present means it will pick acme_version 2 def test_external_account_required_true(self): self.response.json.return_value = messages.Directory({ 'newNonce': 'http://letsencrypt-test.com/acme/new-nonce', 'meta': messages.Directory.Meta(external_account_required=True), }).to_json() client = self._init() self.assertTrue(client.external_account_required()) # newNonce present means it will pick acme_version 2 def test_external_account_required_false(self): self.response.json.return_value = messages.Directory({ 'newNonce': 'http://letsencrypt-test.com/acme/new-nonce', 'meta': messages.Directory.Meta(external_account_required=False), }).to_json() client = self._init() self.assertFalse(client.external_account_required()) def test_external_account_required_false_v1(self): self.response.json.return_value = messages.Directory({ 'meta': messages.Directory.Meta(external_account_required=False), }).to_json() client = self._init() self.assertFalse(client.external_account_required()) class ClientTest(ClientTestBase): """Tests for acme.client.Client.""" def setUp(self): super().setUp() self.directory = DIRECTORY_V1 # Registration self.regr = self.regr.update( terms_of_service='https://www.letsencrypt-demo.org/tos') # Request issuance self.certr = messages.CertificateResource( body=messages_test.CERT, authzrs=(self.authzr,), uri='https://www.letsencrypt-demo.org/acme/cert/1', cert_chain_uri='https://www.letsencrypt-demo.org/ca') from acme.client import Client self.client = Client( directory=self.directory, key=KEY, alg=jose.RS256, net=self.net) def test_init_downloads_directory(self): uri = 'http://www.letsencrypt-demo.org/directory' from acme.client import Client self.client = Client( directory=uri, key=KEY, alg=jose.RS256, net=self.net) self.net.get.assert_called_once_with(uri) @mock.patch('acme.client.ClientNetwork') def test_init_without_net(self, mock_net): mock_net.return_value = mock.sentinel.net alg = jose.RS256 from acme.client import Client self.client = Client( directory=self.directory, key=KEY, alg=alg) mock_net.called_once_with(KEY, alg=alg, verify_ssl=True) self.assertEqual(self.client.net, mock.sentinel.net) def test_register(self): # "Instance of 'Field' has no to_json/update member" bug: 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': self.regr.terms_of_service}, }) self.assertEqual(self.regr, self.client.register(self.new_reg)) # TODO: test POST call arguments 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() self.assertEqual(self.regr, self.client.update_registration(self.regr)) # TODO: test POST call arguments # TODO: split here and separate test self.response.json.return_value = self.regr.body.update( contact=()).to_json() def test_deactivate_account(self): self.response.headers['Location'] = self.regr.uri self.response.json.return_value = self.regr.body.to_json() self.assertEqual(self.regr, self.client.deactivate_registration(self.regr)) def test_query_registration(self): self.response.json.return_value = self.regr.body.to_json() self.assertEqual(self.regr, self.client.query_registration(self.regr)) def test_agree_to_tos(self): self.client.update_registration = mock.Mock() self.client.agree_to_tos(self.regr) regr = self.client.update_registration.call_args[0][0] self.assertEqual(self.regr.terms_of_service, regr.body.agreement) def _prepare_response_for_request_challenges(self): self.response.status_code = http_client.CREATED self.response.headers['Location'] = self.authzr.uri self.response.json.return_value = self.authz.to_json() def test_request_challenges(self): self._prepare_response_for_request_challenges() self.client.request_challenges(self.identifier) self.net.post.assert_called_once_with( self.directory.new_authz, messages.NewAuthorization(identifier=self.identifier), acme_version=1) def test_request_challenges_deprecated_arg(self): self._prepare_response_for_request_challenges() self.client.request_challenges(self.identifier, new_authzr_uri="hi") self.net.post.assert_called_once_with( self.directory.new_authz, messages.NewAuthorization(identifier=self.identifier), acme_version=1) def test_request_challenges_custom_uri(self): self._prepare_response_for_request_challenges() self.client.request_challenges(self.identifier) self.net.post.assert_called_once_with( 'https://www.letsencrypt-demo.org/acme/new-authz', mock.ANY, acme_version=1) def test_request_challenges_unexpected_update(self): self._prepare_response_for_request_challenges() self.response.json.return_value = self.authz.update( identifier=self.identifier.update(value='foo')).to_json() self.assertRaises( errors.UnexpectedUpdate, self.client.request_challenges, self.identifier) def test_request_challenges_wildcard(self): wildcard_identifier = messages.Identifier( typ=messages.IDENTIFIER_FQDN, value='*.example.org') self.assertRaises( errors.WildcardUnsupportedError, self.client.request_challenges, wildcard_identifier) def test_request_domain_challenges(self): self.client.request_challenges = mock.MagicMock() self.assertEqual( self.client.request_challenges(self.identifier), self.client.request_domain_challenges('example.com')) def test_answer_challenge(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) # TODO: split here and separate test self.assertRaises(errors.UnexpectedUpdate, self.client.answer_challenge, self.challr.body.update(uri='foo'), chall_response) def test_answer_challenge_missing_next(self): self.assertRaises( errors.ClientError, self.client.answer_challenge, self.challr.body, challenges.DNSResponse(validation=None)) def test_retry_after_date(self): self.response.headers['Retry-After'] = 'Fri, 31 Dec 1999 23:59:59 GMT' self.assertEqual( 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' self.assertEqual( 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" self.assertEqual( 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' self.assertEqual( 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 self.assertEqual( datetime.datetime(2015, 3, 27, 0, 0, 10), self.client.retry_after(response=self.response, default=10)) def test_poll(self): self.response.json.return_value = self.authzr.body.to_json() self.assertEqual((self.authzr, self.response), self.client.poll(self.authzr)) # TODO: split here and separate test self.response.json.return_value = self.authz.update( identifier=self.identifier.update(value='foo')).to_json() self.assertRaises( errors.UnexpectedUpdate, self.client.poll, self.authzr) def test_request_issuance(self): self.response.content = CERT_DER self.response.headers['Location'] = self.certr.uri self.response.links['up'] = {'url': self.certr.cert_chain_uri} self.assertEqual(self.certr, self.client.request_issuance( messages_test.CSR, (self.authzr,))) # TODO: check POST args def test_request_issuance_missing_up(self): self.response.content = CERT_DER self.response.headers['Location'] = self.certr.uri self.assertEqual( self.certr.update(cert_chain_uri=None), self.client.request_issuance(messages_test.CSR, (self.authzr,))) def test_request_issuance_missing_location(self): self.assertRaises( errors.ClientError, self.client.request_issuance, messages_test.CSR, (self.authzr,)) @mock.patch('acme.client.datetime') @mock.patch('acme.client.time') def test_poll_and_request_issuance(self, time_mock, dt_mock): # clock.dt | pylint: disable=no-member clock = mock.MagicMock(dt=datetime.datetime(2015, 3, 27)) def sleep(seconds): """increment clock""" clock.dt += datetime.timedelta(seconds=seconds) time_mock.sleep.side_effect = sleep def now(): """return current clock value""" return clock.dt dt_mock.datetime.now.side_effect = now dt_mock.timedelta = datetime.timedelta def poll(authzr): # pylint: disable=missing-docstring # record poll start time based on the current clock value authzr.times.append(clock.dt) # suppose it takes 2 seconds for server to produce the # result, increment clock clock.dt += datetime.timedelta(seconds=2) if len(authzr.retries) == 1: # no more retries done = mock.MagicMock(uri=authzr.uri, times=authzr.times) done.body.status = authzr.retries[0] return done, [] # response (2nd result tuple element) is reduced to only # Retry-After header contents represented as integer # seconds; authzr.retries is a list of Retry-After # headers, head(retries) is peeled of as a current # Retry-After header, and tail(retries) is persisted for # later poll() calls return (mock.MagicMock(retries=authzr.retries[1:], uri=authzr.uri + '.', times=authzr.times), authzr.retries[0]) self.client.poll = mock.MagicMock(side_effect=poll) mintime = 7 def retry_after(response, default): # pylint: disable=missing-docstring # check that poll_and_request_issuance correctly passes mintime self.assertEqual(default, mintime) return clock.dt + datetime.timedelta(seconds=response) self.client.retry_after = mock.MagicMock(side_effect=retry_after) def request_issuance(csr, authzrs): # pylint: disable=missing-docstring return csr, authzrs self.client.request_issuance = mock.MagicMock( side_effect=request_issuance) csr = mock.MagicMock() authzrs = ( mock.MagicMock(uri='a', times=[], retries=( 8, 20, 30, messages.STATUS_VALID)), mock.MagicMock(uri='b', times=[], retries=( 5, messages.STATUS_VALID)), ) cert, updated_authzrs = self.client.poll_and_request_issuance( csr, authzrs, mintime=mintime, # make sure that max_attempts is per-authorization, rather # than global max_attempts=max(len(authzrs[0].retries), len(authzrs[1].retries))) self.assertIs(cert[0], csr) self.assertIs(cert[1], updated_authzrs) self.assertEqual(updated_authzrs[0].uri, 'a...') self.assertEqual(updated_authzrs[1].uri, 'b.') self.assertEqual(updated_authzrs[0].times, [ datetime.datetime(2015, 3, 27), # a is scheduled for 10, but b is polling [9..11), so it # will be picked up as soon as b is finished, without # additional sleeping datetime.datetime(2015, 3, 27, 0, 0, 11), datetime.datetime(2015, 3, 27, 0, 0, 33), datetime.datetime(2015, 3, 27, 0, 1, 5), ]) self.assertEqual(updated_authzrs[1].times, [ datetime.datetime(2015, 3, 27, 0, 0, 2), datetime.datetime(2015, 3, 27, 0, 0, 9), ]) self.assertEqual(clock.dt, datetime.datetime(2015, 3, 27, 0, 1, 7)) # CA sets invalid | TODO: move to a separate test invalid_authzr = mock.MagicMock( times=[], retries=[messages.STATUS_INVALID]) self.assertRaises( errors.PollError, self.client.poll_and_request_issuance, csr, authzrs=(invalid_authzr,), mintime=mintime) # exceeded max_attempts | TODO: move to a separate test self.assertRaises( errors.PollError, self.client.poll_and_request_issuance, csr, authzrs, mintime=mintime, max_attempts=2) def test_deactivate_authorization(self): authzb = self.authzr.body.update(status=messages.STATUS_DEACTIVATED) self.response.json.return_value = authzb.to_json() authzr = self.client.deactivate_authorization(self.authzr) self.assertEqual(authzb, authzr.body) self.assertEqual(self.client.net.post.call_count, 1) self.assertIn(self.authzr.uri, self.net.post.call_args_list[0][0]) def test_check_cert(self): self.response.headers['Location'] = self.certr.uri self.response.content = CERT_DER self.assertEqual(self.certr.update(body=messages_test.CERT), self.client.check_cert(self.certr)) # TODO: split here and separate test self.response.headers['Location'] = 'foo' self.assertRaises( errors.UnexpectedUpdate, self.client.check_cert, self.certr) def test_check_cert_missing_location(self): self.response.content = CERT_DER self.assertRaises( errors.ClientError, self.client.check_cert, self.certr) def test_refresh(self): self.client.check_cert = mock.MagicMock() self.assertEqual( self.client.check_cert(self.certr), self.client.refresh(self.certr)) def test_fetch_chain_no_up_link(self): self.assertEqual([], self.client.fetch_chain(self.certr.update( cert_chain_uri=None))) def test_fetch_chain_single(self): # pylint: disable=protected-access self.client._get_cert = mock.MagicMock() self.client._get_cert.return_value = ( mock.MagicMock(links={}), "certificate") self.assertEqual([self.client._get_cert(self.certr.cert_chain_uri)[1]], self.client.fetch_chain(self.certr)) def test_fetch_chain_max(self): # pylint: disable=protected-access up_response = mock.MagicMock(links={'up': {'url': 'http://cert'}}) noup_response = mock.MagicMock(links={}) self.client._get_cert = mock.MagicMock() self.client._get_cert.side_effect = [ (up_response, "cert")] * 9 + [(noup_response, "last_cert")] chain = self.client.fetch_chain(self.certr, max_length=10) self.assertEqual(chain, ["cert"] * 9 + ["last_cert"]) def test_fetch_chain_too_many(self): # recursive # pylint: disable=protected-access response = mock.MagicMock(links={'up': {'url': 'http://cert'}}) self.client._get_cert = mock.MagicMock() self.client._get_cert.return_value = (response, "certificate") self.assertRaises(errors.Error, self.client.fetch_chain, self.certr) def test_revoke(self): self.client.revoke(self.certr.body, self.rsn) self.net.post.assert_called_once_with( self.directory[messages.Revocation], mock.ANY, acme_version=1) def test_revocation_payload(self): obj = messages.Revocation(certificate=self.certr.body, reason=self.rsn) self.assertIn('reason', obj.to_partial_json().keys()) self.assertEqual(self.rsn, obj.to_partial_json()['reason']) def test_revoke_bad_status_raises_error(self): self.response.status_code = http_client.METHOD_NOT_ALLOWED self.assertRaises( errors.ClientError, self.client.revoke, self.certr, self.rsn) class ClientV2Test(ClientTestBase): """Tests for acme.client.ClientV2.""" def setUp(self): super().setUp() self.directory = DIRECTORY_V2 from acme.client import ClientV2 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) 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 self.assertEqual(self.regr, self.client.new_account(self.new_reg)) def test_new_account_conflict(self): self.response.status_code = http_client.OK self.response.headers['Location'] = self.regr.uri self.assertRaises(errors.ConflictError, self.client.new_account, self.new_reg) 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) self.assertEqual(self.client.new_order(CSR_MIXED_PEM), self.orderr) @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) self.assertEqual(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()] self.assertRaises( 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() self.assertRaises( 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()) self.assertEqual(self.client.poll_authorizations(self.orderr, deadline), updated_orderr) def test_finalize_order_success(self): updated_order = self.order.update( certificate='https://www.letsencrypt-demo.org/acme/cert/') 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) self.assertEqual(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')) self.response.json.return_value = updated_order.to_json() deadline = datetime.datetime(9999, 9, 9) self.assertRaises(errors.IssuanceError, self.client.finalize_order, self.orderr, deadline) def test_finalize_order_timeout(self): deadline = datetime.datetime.now() - datetime.timedelta(seconds=60) self.assertRaises(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/', ) 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, acme_version=2, new_nonce_url=mock.ANY) self.net.post.assert_any_call('https://example.com/acme/cert/2', mock.ANY, acme_version=2, new_nonce_url=mock.ANY) self.assertEqual(resp, updated_orderr) del self.response.headers['Link'] resp = self.client.finalize_order(self.orderr, deadline, fetch_alternative_chains=True) self.assertEqual(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, acme_version=2, new_nonce_url=DIRECTORY_V2['newNonce']) 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() self.assertEqual(self.regr, self.client.update_registration(self.regr)) self.assertIsNotNone(self.client.net.account) self.assertEqual(self.client.net.post.call_count, 2) self.assertIn(DIRECTORY_V2.newAccount, 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) }) self.assertTrue(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) }) self.assertFalse(self.client.external_account_required()) def test_external_account_required_default(self): self.assertFalse(self.client.external_account_required()) 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, acme_version=2, new_nonce_url='https://www.letsencrypt-demo.org/acme/new-nonce') self.client.net.get.assert_not_called() class MockJSONDeSerializable(VersionedLEACMEMixin, 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) from acme.client import ClientNetwork 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): self.assertIs(self.net.verify_ssl, 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", acme_version=1) jws = acme_jws.JWS.json_loads(jws_dump) self.assertEqual(json.loads(jws.payload.decode()), {'foo': 'foo'}) self.assertEqual(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", acme_version=2) jws = acme_jws.JWS.json_loads(jws_dump) self.assertEqual(json.loads(jws.payload.decode()), {'foo': 'foo'}) self.assertEqual(jws.signature.combined.nonce, b'Tg') self.assertEqual(jws.signature.combined.kid, u'acct-uri') self.assertEqual(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 self.assertRaises( 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 self.assertRaises( 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 self.assertRaises( 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 self.assertRaises( 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 self.assertEqual( 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 self.assertEqual(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 self.assertEqual(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 self.assertRaises(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 self.assertEqual( 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 self.assertEqual(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, headers={"Content-Type": "application/pkix-cert"}, content=b"hi") # pylint: disable=protected-access self.net._send_request('HEAD', 'http://example.com/', 'foo', timeout=mock.ANY, bar='baz') mock_logger.debug.assert_called_with( 'Received response:\nHTTP %d\n%s\n\n%s', 200, 'Content-Type: application/pkix-cert', 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 self.assertEqual(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 self.assertEqual( 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 self.assertRaises(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: self.assertEqual("Requesting localhost/nonexistent: " "Connection refused", str(y)) # Requests Library Exceptions except requests.exceptions.ConnectionError as z: #pragma: no cover self.assertTrue("'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): from acme.client import ClientNetwork 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 self.assertNotIn("new_nonce_url", 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 self.assertEqual(self.response, response) self.assertEqual(self.content_type, content_type) self.assertTrue(self.response.ok) self.response.checked = True return self.response def test_head(self): self.assertEqual(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): self.assertEqual(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): self.assertEqual(self.response, self.net.get( 'http://example.com/', content_type=self.content_type, bar='baz')) self.assertTrue(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 self.assertEqual(self.response, self.net.post('uri', self.obj)) self.assertTrue(self.response.checked) def test_post(self): # pylint: disable=protected-access self.assertEqual(self.response, self.net.post( 'uri', self.obj, content_type=self.content_type)) self.assertTrue(self.response.checked) self.net._wrap_in_jws.assert_called_once_with( self.obj, jose.b64decode(self.all_nonces.pop()), "uri", 1) self.available_nonces = [] self.assertRaises(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", 1) def test_post_wrong_initial_nonce(self): # HEAD self.available_nonces = [b'f', jose.b64encode(b'good')] self.assertRaises(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'] self.assertRaises(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 self.assertRaises(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 self.assertRaises(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 self.assertEqual(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: self.assertRaises( requests.exceptions.RequestException, method, 'GET', 'uri') self.assertRaises(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 self.assertRaises(errors.ClientError, self.net.post, 'uri', self.obj, content_type=self.content_type, acme_version=2, new_nonce_url='new_nonce_uri') self.assertEqual(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, acme_version=2, new_nonce_url='new_nonce_uri') class ClientNetworkSourceAddressBindingTest(unittest.TestCase): """Tests that if ClientNetwork has a source IP set manually, the underlying library has used the provided source address.""" def setUp(self): self.source_address = "8.8.8.8" def test_source_address_set(self): from acme.client import ClientNetwork net = ClientNetwork(key=None, alg=None, source_address=self.source_address) for adapter in net.session.adapters.values(): self.assertIn(self.source_address, adapter.source_address) def test_behavior_assumption(self): """This is a test that guardrails the HTTPAdapter behavior so that if the default for a Session() changes, the assumptions here aren't violated silently.""" from acme.client import ClientNetwork # Source address not specified, so the default adapter type should be bound -- this # test should fail if the default adapter type is changed by requests net = ClientNetwork(key=None, alg=None) session = requests.Session() for scheme in session.adapters: client_network_adapter = net.session.adapters.get(scheme) default_adapter = session.adapters.get(scheme) self.assertEqual(client_network_adapter.__class__, default_adapter.__class__) if __name__ == '__main__': unittest.main() # pragma: no cover ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1635888438.0 acme-1.21.0/tests/crypto_util_test.py0000644000076500000240000003213200000000000016302 0ustar00bmwstaff"""Tests for acme.crypto_util.""" import itertools import ipaddress import socket import socketserver import threading import time import unittest from typing import List import josepy as jose import OpenSSL from acme import errors import test_util class SSLSocketAndProbeSNITest(unittest.TestCase): """Tests for acme.crypto_util.SSLSocket/probe_sni.""" def setUp(self): self.cert = test_util.load_comparable_cert('rsa2048_cert.pem') key = test_util.load_pyopenssl_private_key('rsa2048_key.pem') # pylint: disable=protected-access certs = {b'foo': (key, self.cert.wrapped)} from acme.crypto_util import SSLSocket class _TestServer(socketserver.TCPServer): def server_bind(self): # pylint: disable=missing-docstring self.socket = SSLSocket(socket.socket(), certs) socketserver.TCPServer.server_bind(self) 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 def _probe(self, name): from acme.crypto_util import probe_sni return jose.ComparableX509(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() self.assertEqual(self.cert, self._probe(b'foo')) def test_probe_not_recognized_name(self): self._start_server() self.assertRaises(errors.Error, self._probe, b'bar') def test_probe_connection_error(self): self.server.server_close() original_timeout = socket.getdefaulttimeout() try: socket.setdefaulttimeout(1) self.assertRaises(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 self.assertRaises(ValueError): _ = SSLSocket(None, {'sni': ('key', 'cert')}, cert_selection=lambda _: None) with self.assertRaises(ValueError): _ = SSLSocket(None) class PyOpenSSLCertOrReqAllNamesTest(unittest.TestCase): """Test for acme.crypto_util._pyopenssl_cert_or_req_all_names.""" @classmethod def _call(cls, loader, name): # pylint: disable=protected-access from acme.crypto_util import _pyopenssl_cert_or_req_all_names return _pyopenssl_cert_or_req_all_names(loader(name)) def _call_cert(self, name): return self._call(test_util.load_cert, name) def test_cert_one_san_no_common(self): self.assertEqual(self._call_cert('cert-nocn.der'), ['no-common-name.badssl.com']) def test_cert_no_sans_yes_common(self): self.assertEqual(self._call_cert('cert.pem'), ['example.com']) def test_cert_two_sans_yes_common(self): self.assertEqual(self._call_cert('cert-san.pem'), ['example.com', 'www.example.com']) class PyOpenSSLCertOrReqSANTest(unittest.TestCase): """Test for acme.crypto_util._pyopenssl_cert_or_req_san.""" @classmethod def _call(cls, loader, name): # pylint: disable=protected-access from acme.crypto_util import _pyopenssl_cert_or_req_san return _pyopenssl_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): self.assertEqual(self._call_cert('cert.pem'), []) def test_cert_two_sans(self): self.assertEqual(self._call_cert('cert-san.pem'), ['example.com', 'www.example.com']) def test_cert_hundred_sans(self): self.assertEqual(self._call_cert('cert-100sans.pem'), ['example{0}.com'.format(i) for i in range(1, 101)]) def test_cert_idn_sans(self): self.assertEqual(self._call_cert('cert-idnsans.pem'), self._get_idn_names()) def test_csr_no_sans(self): self.assertEqual(self._call_csr('csr-nosans.pem'), []) def test_csr_one_san(self): self.assertEqual(self._call_csr('csr.pem'), ['example.com']) def test_csr_two_sans(self): self.assertEqual(self._call_csr('csr-san.pem'), ['example.com', 'www.example.com']) def test_csr_six_sans(self): self.assertEqual(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): self.assertEqual(self._call_csr('csr-100sans.pem'), ['example{0}.com'.format(i) for i in range(1, 101)]) def test_csr_idn_sans(self): self.assertEqual(self._call_csr('csr-idnsans.pem'), self._get_idn_names()) def test_critical_san(self): self.assertEqual(self._call_cert('critical-san.pem'), ['chicago-cubs.venafi.example', 'cubs.venafi.example']) class PyOpenSSLCertOrReqSANIPTest(unittest.TestCase): """Test for acme.crypto_util._pyopenssl_cert_or_req_san_ip.""" @classmethod def _call(cls, loader, name): # pylint: disable=protected-access from acme.crypto_util import _pyopenssl_cert_or_req_san_ip return _pyopenssl_cert_or_req_san_ip(loader(name)) 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): self.assertEqual(self._call_cert('cert.pem'), []) def test_csr_no_sans(self): self.assertEqual(self._call_csr('csr-nosans.pem'), []) def test_cert_domain_sans(self): self.assertEqual(self._call_cert('cert-san.pem'), []) def test_csr_domain_sans(self): self.assertEqual(self._call_csr('csr-san.pem'), []) def test_cert_ip_two_sans(self): self.assertEqual(self._call_cert('cert-ipsans.pem'), ['192.0.2.145', '203.0.113.1']) def test_csr_ip_two_sans(self): self.assertEqual(self._call_csr('csr-ipsans.pem'), ['192.0.2.145', '203.0.113.1']) def test_csr_ipv6_sans(self): self.assertEqual(self._call_csr('csr-ipv6sans.pem'), ['0:0:0:0:0:0:0:1', 'A3BE:32F3:206E:C75D:956:CEE:9858:5EC5']) def test_cert_ipv6_sans(self): self.assertEqual(self._call_cert('cert-ipv6sans.pem'), ['0:0:0:0:0:0:0:1', 'A3BE:32F3:206E:C75D:956:CEE:9858:5EC5']) class GenSsCertTest(unittest.TestCase): """Test for gen_ss_cert (generation of self-signed cert).""" def setUp(self): self.cert_count = 5 self.serial_num: List[int] = [] self.key = OpenSSL.crypto.PKey() self.key.generate_key(OpenSSL.crypto.TYPE_RSA, 2048) def test_sn_collisions(self): from acme.crypto_util import gen_ss_cert for _ in range(self.cert_count): cert = gen_ss_cert(self.key, ['dummy'], force_san=True, ips=[ipaddress.ip_address("10.10.10.10")]) self.serial_num.append(cert.get_serial_number()) self.assertGreaterEqual(len(set(self.serial_num)), self.cert_count) def test_no_name(self): from acme.crypto_util import gen_ss_cert with self.assertRaises(AssertionError): gen_ss_cert(self.key, ips=[ipaddress.ip_address("1.1.1.1")]) gen_ss_cert(self.key) class MakeCSRTest(unittest.TestCase): """Test for standalone functions.""" @classmethod def _call_with_key(cls, *args, **kwargs): privkey = OpenSSL.crypto.PKey() privkey.generate_key(OpenSSL.crypto.TYPE_RSA, 2048) privkey_pem = OpenSSL.crypto.dump_privatekey(OpenSSL.crypto.FILETYPE_PEM, privkey) 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"]) self.assertIn(b'--BEGIN CERTIFICATE REQUEST--', csr_pem) self.assertIn(b'--END CERTIFICATE REQUEST--', csr_pem) csr = OpenSSL.crypto.load_certificate_request( OpenSSL.crypto.FILETYPE_PEM, csr_pem) # In pyopenssl 0.13 (used with TOXENV=py27-oldest), csr objects don't # have a get_extensions() method, so we skip this test if the method # isn't available. if hasattr(csr, 'get_extensions'): self.assertEqual(len(csr.get_extensions()), 1) self.assertEqual(csr.get_extensions()[0].get_data(), OpenSSL.crypto.X509Extension( b'subjectAltName', critical=False, value=b'DNS:a.example, DNS:b.example', ).get_data(), ) 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')]) self.assertIn(b'--BEGIN CERTIFICATE REQUEST--' , csr_pem) self.assertIn(b'--END CERTIFICATE REQUEST--' , csr_pem) csr = OpenSSL.crypto.load_certificate_request( OpenSSL.crypto.FILETYPE_PEM, csr_pem) # In pyopenssl 0.13 (used with TOXENV=py27-oldest), csr objects don't # have a get_extensions() method, so we skip this test if the method # isn't available. if hasattr(csr, 'get_extensions'): self.assertEqual(len(csr.get_extensions()), 1) self.assertEqual(csr.get_extensions()[0].get_data(), OpenSSL.crypto.X509Extension( b'subjectAltName', critical=False, value=b'DNS:a.example, IP:127.0.0.1, IP:::1', ).get_data(), ) # for IP san it's actually need to be octet-string, # but somewhere downstream thankfully handle it for us def test_make_csr_must_staple(self): csr_pem = self._call_with_key(["a.example"], must_staple=True) csr = OpenSSL.crypto.load_certificate_request( OpenSSL.crypto.FILETYPE_PEM, csr_pem) # In pyopenssl 0.13 (used with TOXENV=py27-oldest), csr objects don't # have a get_extensions() method, so we skip this test if the method # isn't available. if hasattr(csr, 'get_extensions'): self.assertEqual(len(csr.get_extensions()), 2) # NOTE: Ideally we would filter by the TLS Feature OID, but # OpenSSL.crypto.X509Extension doesn't give us the extension's raw OID, # and the shortname field is just "UNDEF" must_staple_exts = [e for e in csr.get_extensions() if e.get_data() == b"0\x03\x02\x01\x05"] self.assertEqual(len(must_staple_exts), 1, "Expected exactly one Must Staple extension") def test_make_csr_without_hostname(self): self.assertRaises(ValueError, self._call_with_key) class DumpPyopensslChainTest(unittest.TestCase): """Test for dump_pyopenssl_chain.""" @classmethod def _call(cls, loaded): # pylint: disable=protected-access from acme.crypto_util import dump_pyopenssl_chain return dump_pyopenssl_chain(loaded) def test_dump_pyopenssl_chain(self): names = ['cert.pem', 'cert-san.pem', 'cert-idnsans.pem'] loaded = [test_util.load_cert(name) for name in names] length = sum( len(OpenSSL.crypto.dump_certificate(OpenSSL.crypto.FILETYPE_PEM, cert)) for cert in loaded) self.assertEqual(len(self._call(loaded)), length) def test_dump_pyopenssl_chain_wrapped(self): names = ['cert.pem', 'cert-san.pem', 'cert-idnsans.pem'] loaded = [test_util.load_cert(name) for name in names] wrap_func = jose.ComparableX509 wrapped = [wrap_func(cert) for cert in loaded] dump_func = OpenSSL.crypto.dump_certificate length = sum(len(dump_func(OpenSSL.crypto.FILETYPE_PEM, cert)) for cert in loaded) self.assertEqual(len(self._call(wrapped)), length) if __name__ == '__main__': unittest.main() # pragma: no cover ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1635888438.0 acme-1.21.0/tests/errors_test.py0000644000076500000240000000276000000000000015245 0ustar00bmwstaff"""Tests for acme.errors.""" import unittest from unittest import mock 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): self.assertEqual("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): self.assertIn("FOO", str(self.error)) self.assertIn("{}", 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): self.assertTrue(self.timeout.timeout) self.assertFalse(self.invalid.timeout) def test_repr(self): self.assertEqual('PollError(exhausted=%s, updated={sentinel.AR: ' 'sentinel.AR2})' % repr(set()), repr(self.invalid)) if __name__ == "__main__": unittest.main() # pragma: no cover ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1635888438.0 acme-1.21.0/tests/fields_test.py0000644000076500000240000000404200000000000015172 0ustar00bmwstaff"""Tests for acme.fields.""" import datetime import unittest import josepy as jose 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): self.assertEqual('x', self.field.decode('x')) def test_decode_bad(self): self.assertRaises(jose.DeserializationError, self.field.decode, 'y') def test_encode(self): self.assertEqual('x', self.field.encode('x')) def test_encode_override(self): self.assertEqual('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 self.assertEqual( self.encoded, RFC3339Field.default_encoder(self.decoded)) def test_default_encoder_naive_fails(self): from acme.fields import RFC3339Field self.assertRaises( ValueError, RFC3339Field.default_encoder, datetime.datetime.now()) def test_default_decoder(self): from acme.fields import RFC3339Field self.assertEqual( self.decoded, RFC3339Field.default_decoder(self.encoded)) def test_default_decoder_raises_deserialization_error(self): from acme.fields import RFC3339Field self.assertRaises( jose.DeserializationError, RFC3339Field.default_decoder, '') class ResourceTest(unittest.TestCase): """Tests for acme.fields.Resource.""" def setUp(self): from acme.fields import Resource self.field = Resource('x') def test_decode_good(self): self.assertEqual('x', self.field.decode('x')) def test_decode_wrong(self): self.assertRaises(jose.DeserializationError, self.field.decode, 'y') if __name__ == '__main__': unittest.main() # pragma: no cover ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1635888438.0 acme-1.21.0/tests/jose_test.py0000644000076500000240000000377300000000000014676 0ustar00bmwstaff"""Tests for acme.jose shim.""" import importlib import unittest class JoseTest(unittest.TestCase): """Tests for acme.jose shim.""" def _test_it(self, 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) self.assertIs(acme_jose_mod, josepy_mod) self.assertIs(getattr(acme_jose_mod, attribute), getattr(josepy_mod, attribute)) # We use the imports below with eval, but pylint doesn't # understand that. import acme # pylint: disable=unused-import import josepy # pylint: disable=unused-import acme_jose_mod = eval(acme_jose_path) # pylint: disable=eval-used josepy_mod = eval(josepy_path) # pylint: disable=eval-used self.assertIs(acme_jose_mod, josepy_mod) self.assertIs(getattr(acme_jose_mod, attribute), getattr(josepy_mod, attribute)) def test_top_level(self): self._test_it('', 'RS512') def test_submodules(self): # 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: self._test_it(mod, attr) if __name__ == '__main__': unittest.main() # pragma: no cover ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1635888438.0 acme-1.21.0/tests/jws_test.py0000644000076500000240000000401400000000000014526 0ustar00bmwstaff"""Tests for acme.jws.""" import unittest import josepy as jose 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: assert False # pragma: no cover def test_nonce_decoder(self): from acme.jws import Header nonce_field = Header._fields['nonce'] self.assertRaises( jose.DeserializationError, nonce_field.decode, self.wrong_nonce) self.assertEqual(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) self.assertEqual(jws.signature.combined.nonce, self.nonce) self.assertEqual(jws.signature.combined.url, self.url) self.assertEqual(jws.signature.combined.kid, self.kid) self.assertIsNone(jws.signature.combined.jwk) # TODO: check that nonce is in protected header self.assertEqual(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) self.assertIsNone(jws.signature.combined.kid) self.assertEqual(jws.signature.combined.jwk, self.pubkey) if __name__ == '__main__': unittest.main() # pragma: no cover ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1635888438.0 acme-1.21.0/tests/magic_typing_test.py0000644000076500000240000000200400000000000016372 0ustar00bmwstaff"""Tests for acme.magic_typing.""" import sys import unittest import warnings from unittest import mock class MagicTypingTest(unittest.TestCase): """Tests for acme.magic_typing.""" def test_import_success(self): try: import typing as temp_typing except ImportError: # pragma: no cover temp_typing = None # pragma: no cover typing_class_mock = mock.MagicMock() text_mock = mock.MagicMock() typing_class_mock.Text = text_mock sys.modules['typing'] = typing_class_mock if 'acme.magic_typing' in sys.modules: del sys.modules['acme.magic_typing'] # pragma: no cover with warnings.catch_warnings(): warnings.filterwarnings("ignore", category=DeprecationWarning) from acme.magic_typing import Text self.assertEqual(Text, text_mock) del sys.modules['acme.magic_typing'] sys.modules['typing'] = temp_typing if __name__ == '__main__': unittest.main() # pragma: no cover ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1635888438.0 acme-1.21.0/tests/messages_test.py0000644000076500000240000004212100000000000015533 0ustar00bmwstaff"""Tests for acme.messages.""" from typing import Dict import unittest from unittest import mock import josepy as jose from acme import challenges import test_util CERT = test_util.load_comparable_cert('cert.der') CSR = test_util.load_comparable_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, ERROR_PREFIX 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.empty_error = Error() def test_default_typ(self): from acme.messages import Error self.assertEqual(Error().typ, 'about:blank') def test_from_json_empty(self): from acme.messages import Error self.assertEqual(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_description(self): self.assertEqual('The request message was malformed', self.error.description) self.assertIsNone(self.error_custom.description) def test_code(self): from acme.messages import Error self.assertEqual('malformed', self.error.code) self.assertIsNone(self.error_custom.code) self.assertIsNone(Error().code) def test_is_acme_error(self): from acme.messages import is_acme_error, Error self.assertTrue(is_acme_error(self.error)) self.assertFalse(is_acme_error(self.error_custom)) self.assertFalse(is_acme_error(Error())) self.assertFalse(is_acme_error(self.empty_error)) self.assertFalse(is_acme_error("must pet all the {dogs|rabbits}")) def test_unicode_error(self): from acme.messages import Error, is_acme_error arabic_error = Error.with_code( 'malformed', detail=u'\u0639\u062f\u0627\u0644\u0629', title='title') self.assertTrue(is_acme_error(arabic_error)) def test_with_code(self): from acme.messages import Error, is_acme_error self.assertTrue(is_acme_error(Error.with_code('badCSR'))) self.assertRaises(ValueError, Error.with_code, 'not an ACME error code') def test_str(self): self.assertEqual( str(self.error), u"{0.typ} :: {0.description} :: {0.detail} :: {0.title}" .format(self.error)) 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): self.assertEqual('a', self.const_a.to_partial_json()) self.assertEqual('b', self.const_b.to_partial_json()) def test_from_json(self): self.assertEqual(self.const_a, self.MockConstant.from_json('a')) self.assertRaises( jose.DeserializationError, self.MockConstant.from_json, 'c') def test_from_json_hashable(self): hash(self.MockConstant.from_json('a')) def test_repr(self): self.assertEqual('MockConstant(a)', repr(self.const_a)) self.assertEqual('MockConstant(b)', repr(self.const_b)) def test_equality(self): const_a_prime = self.MockConstant('a') self.assertNotEqual(self.const_a, self.const_b) self.assertEqual(self.const_a, const_a_prime) self.assertNotEqual(self.const_a, self.const_b) self.assertEqual(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({ 'new-reg': 'reg', mock.MagicMock(resource_type='new-cert'): 'cert', 'meta': Directory.Meta( terms_of_service='https://example.com/acme/terms', website='https://www.example.com/', caa_identities=['example.com'], ), }) 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): self.assertEqual('reg', self.dir['new-reg']) from acme.messages import NewRegistration self.assertEqual('reg', self.dir[NewRegistration]) self.assertEqual('reg', self.dir[NewRegistration()]) def test_getitem_fails_with_key_error(self): self.assertRaises(KeyError, self.dir.__getitem__, 'foo') def test_getattr(self): self.assertEqual('reg', self.dir.new_reg) def test_getattr_fails_with_attribute_error(self): self.assertRaises(AttributeError, self.dir.__getattr__, 'foo') def test_to_json(self): self.assertEqual(self.dir.to_json(), { 'new-reg': 'reg', 'new-cert': 'cert', 'meta': { 'terms-of-service': 'https://example.com/acme/terms', 'website': 'https://www.example.com/', 'caaIdentities': ['example.com'], }, }) 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' self.assertTrue(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) self.assertEqual(len(eab), 3) self.assertEqual(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') self.assertEqual(reg.contact, ( 'tel:1234', 'mailto:admin@foo.com', )) def test_new_registration_from_data_with_eab(self): from acme.messages import NewRegistration, ExternalAccountBinding, Directory 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) self.assertEqual(reg.contact, ( 'mailto:admin@foo.com', )) self.assertEqual(sorted(reg.external_account_binding.keys()), sorted(['protected', 'payload', 'signature'])) def test_phones(self): self.assertEqual(('1234',), self.reg.phones) def test_emails(self): self.assertEqual(('admin@foo.com',), self.reg.emails) def test_to_partial_json(self): self.assertEqual(self.jobj_to, self.reg.to_partial_json()) def test_from_json(self): from acme.messages import Registration self.assertEqual(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=()) self.assertEqual(empty_new_reg.contact, ()) self.assertEqual(new_reg_with_contact.contact, ()) self.assertNotIn('contact', empty_new_reg.to_partial_json()) self.assertNotIn('contact', empty_new_reg.fields_to_partial_json()) self.assertIn('contact', new_reg_with_contact.to_partial_json()) self.assertIn('contact', 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"}' self.assertEqual(jstring, UpdateRegistration().json_dumps()) self.assertEqual( 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): self.assertEqual(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 self.assertEqual('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 = { 'uri': '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): self.assertEqual(self.challb.encode('uri'), self.challb.uri) def test_to_partial_json(self): self.assertEqual(self.jobj_to, self.challb.to_partial_json()) def test_from_json(self): from acme.messages import ChallengeBody self.assertEqual(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): self.assertEqual(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')), ) combinations = ((0,), (1,)) 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, combinations=combinations, challenges=self.challbs) self.jobj_from = { 'identifier': identifier.to_json(), 'challenges': [challb.to_json() for challb in self.challbs], 'combinations': combinations, } 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)) def test_resolved_combinations(self): self.assertEqual(self.authz.resolved_combinations, ( (self.challbs[0],), (self.challbs[1],), )) 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) self.assertIsInstance(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): self.assertIsInstance(self.req, jose.JSONDeSerializable) from acme.messages import CertificateRequest self.assertEqual( 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): self.assertIsInstance(self.certr, jose.JSONDeSerializable) from acme.messages import CertificateResource self.assertEqual( 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): self.assertEqual(self.regr.to_json(), { 'body': mock.sentinel.body, 'uri': mock.sentinel.uri, 'authorizations': None, }) class NewOrderTest(unittest.TestCase): """Tests for acme.messages.NewOrder.""" def setUp(self): from acme.messages import NewOrder self.reg = NewOrder( identifiers=mock.sentinel.identifiers) def test_to_partial_json(self): self.assertEqual(self.reg.to_json(), { 'identifiers': mock.sentinel.identifiers, }) 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() new_order.le_acme_version = 2 jobj = new_order.json_dumps(indent=2).encode() # RFC8555 states that JWS bodies must not have a resource field. self.assertEqual(jobj, b'{}') if __name__ == '__main__': unittest.main() # pragma: no cover ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1635888438.0 acme-1.21.0/tests/standalone_test.py0000644000076500000240000002307400000000000016062 0ustar00bmwstaff"""Tests for acme.standalone.""" import http.client as http_client import socket import socketserver import threading import unittest from typing import Set from unittest import mock import josepy as jose import requests from acme import challenges from acme import crypto_util from acme import errors 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() def test_index(self): response = requests.get( 'http://localhost:{0}'.format(self.port), verify=False) self.assertEqual( response.text, 'ACME client standalone challenge solver') self.assertTrue(response.ok) def test_404(self): response = requests.get( 'http://localhost:{0}/foo'.format(self.port), verify=False) self.assertEqual(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): self.assertTrue(self._test_http01(add=True)) def test_http01_not_found(self): self.assertFalse(self._test_http01(add=False)) def test_timely_shutdown(self): from acme.standalone import HTTP01Server server = HTTP01Server(('', 0), resources=set(), timeout=0.05) server_thread = threading.Thread(target=server.serve_forever) server_thread.start() client = socket.socket() 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 self.assertFalse(is_hung, msg='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': ( test_util.load_pyopenssl_private_key('rsa2048_key.pem'), test_util.load_cert('rsa2048_cert.pem'), )} # Use different certificate for challenge. self.challenge_certs = {b'localhost': ( test_util.load_pyopenssl_private_key('rsa4096_key.pem'), test_util.load_cert('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() # 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(jose.ComparableX509(cert), # jose.ComparableX509(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. self.assertEqual( jose.ComparableX509(cert), jose.ComparableX509(self.challenge_certs[b'localhost'][1]) ) def test_bad_alpn(self): host, port = self.server.socket.getsockname()[:2] with self.assertRaises(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 socketserver.TCPServer.__init__(self, *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 = socket.error(EADDRINUSE, "Fake addr in use error") with self.assertRaises(socket.error) as em: BaseDualNetworkedServers( BaseDualNetworkedServersTest.SingleProtocolServer, ('', 0), socketserver.BaseRequestHandler) self.assertEqual(em.exception.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: self.assertEqual(prev_port, port) prev_port = port 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) self.assertEqual( response.text, 'ACME client standalone challenge solver') self.assertTrue(response.ok) def test_404(self): response = requests.get( 'http://localhost:{0}/foo'.format(self.port), verify=False) self.assertEqual(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): self.assertTrue(self._test_http01(add=True)) def test_http01_not_found(self): self.assertFalse(self._test_http01(add=False)) if __name__ == "__main__": unittest.main() # pragma: no cover ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1635888438.0 acme-1.21.0/tests/test_util.py0000644000076500000240000000376400000000000014713 0ustar00bmwstaff"""Test utilities. .. warning:: This module is not part of the public API. """ import os from cryptography.hazmat.backends import default_backend from cryptography.hazmat.primitives import serialization import josepy as jose from OpenSSL import crypto import pkg_resources def load_vector(*names): """Load contents of a test vector.""" # luckily, resource_string opens file in binary mode return pkg_resources.resource_string( __name__, os.path.join('testdata', *names)) def _guess_loader(filename, loader_pem, loader_der): _, ext = os.path.splitext(filename) if ext.lower() == '.pem': return loader_pem elif ext.lower() == '.der': return loader_der raise ValueError("Loader could not be recognized based on extension") # pragma: no cover def load_cert(*names): """Load certificate.""" loader = _guess_loader( names[-1], crypto.FILETYPE_PEM, crypto.FILETYPE_ASN1) return crypto.load_certificate(loader, load_vector(*names)) def load_comparable_cert(*names): """Load ComparableX509 cert.""" return jose.ComparableX509(load_cert(*names)) def load_csr(*names): """Load certificate request.""" loader = _guess_loader( names[-1], crypto.FILETYPE_PEM, crypto.FILETYPE_ASN1) return crypto.load_certificate_request(loader, load_vector(*names)) def load_comparable_csr(*names): """Load ComparableX509 certificate request.""" return jose.ComparableX509(load_csr(*names)) def load_rsa_private_key(*names): """Load RSA private key.""" loader = _guess_loader(names[-1], serialization.load_pem_private_key, serialization.load_der_private_key) return jose.ComparableRSAKey(loader( load_vector(*names), password=None, backend=default_backend())) def load_pyopenssl_private_key(*names): """Load pyOpenSSL private key.""" loader = _guess_loader( names[-1], crypto.FILETYPE_PEM, crypto.FILETYPE_ASN1) return crypto.load_privatekey(loader, load_vector(*names)) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1635888462.3089676 acme-1.21.0/tests/testdata/0000755000076500000240000000000000000000000014124 5ustar00bmwstaff././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1635888438.0 acme-1.21.0/tests/testdata/README0000644000076500000240000000127600000000000015012 0ustar00bmwstaffIn 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 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1635888438.0 acme-1.21.0/tests/testdata/cert-100sans.pem0000644000076500000240000000530000000000000016745 0ustar00bmwstaff-----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=1635888438.0 acme-1.21.0/tests/testdata/cert-idnsans.pem0000644000076500000240000000351200000000000017222 0ustar00bmwstaff-----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=1635888438.0 acme-1.21.0/tests/testdata/cert-ipsans.pem0000644000076500000240000000241100000000000017055 0ustar00bmwstaff-----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=1635888438.0 acme-1.21.0/tests/testdata/cert-ipv6sans.pem0000644000076500000240000000243600000000000017340 0ustar00bmwstaff-----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=1635888438.0 acme-1.21.0/tests/testdata/cert-nocn.der0000644000076500000240000000256500000000000016520 0ustar00bmwstaff0q0Yob6^%0  *H  01 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 ³PpX̓\hI"+,v>lK`LL겴rɆ\A]+qK.P7gaABF't R&!slLyUk=HR(۪n`Jwk9$1MSmB|3H"qzl(7Jlr~ZiWD Gt\A"").I '@rٌWSkT@'spiAEU|d?=,é|Lʅ00U#0+ϭO/*HH*B$0U{ :Giq}7?0U0 U00U%0++0PU I0G0; +10+0)+https://secure.comodo.com/CPS0g 0ZUS0Q0OMKIhttp://crl.comodoca.com/COMODORSAOrganizationValidationSecureServerCA.crl0+0}0U+0Ihttp://crt.comodoca.com/COMODORSAOrganizationValidationSecureServerCA.crt0$+0http://ocsp.comodoca.com0$U0no-common-name.badssl.com0  *H  i%Ff4DH:kD{|p5&J;~Zw-J/wUؔ?3Hb5Vh3-fRjEZm؝,QN?lْ-I0q.gd{҅ӣMBi$Kn:ħ)k!8LFÿ2WiKj,F;{f././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1635888438.0 acme-1.21.0/tests/testdata/cert-san.pem0000644000076500000240000000142200000000000016342 0ustar00bmwstaff-----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=1635888438.0 acme-1.21.0/tests/testdata/cert.der0000644000076500000240000000140300000000000015553 0ustar00bmwstaff00 2WZd0  *H  010U example.com0 151028072441Z 151127072441Z010U example.com0"0  *H 0 |0w3$Gav1.k>=ae\u[DnXߋ":ājP]teMp*8(if3-/e\vui DKlK˷ nal3g3$aLOGL) 3S񪓪̀,˞j s|H"0yc_M=.ۢh5JIh^+WɯFWs;?Gs9_̆eƞT4)kP0N0U6  )+eX70U#06  )+eX70 U00  *H  T2O]J7 ԣN^ x1[IK,=^2k뮫mHG[2-mlhfll@'DpH4C'{[C c60u;d FAzk&eP Ǫa3EuPjW3u ,S~C6Q8YOfs UpM!ȑ)+m,(hE {b KRVhŗ ;lv1././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1635888438.0 acme-1.21.0/tests/testdata/cert.pem0000644000076500000240000000130500000000000015563 0ustar00bmwstaff-----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=1635888438.0 acme-1.21.0/tests/testdata/critical-san.pem0000644000076500000240000000322300000000000017200 0ustar00bmwstaff-----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=1635888438.0 acme-1.21.0/tests/testdata/csr-100sans.pem0000644000076500000240000000502100000000000016577 0ustar00bmwstaff-----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=1635888438.0 acme-1.21.0/tests/testdata/csr-6sans.pem0000644000076500000240000000124400000000000016447 0ustar00bmwstaff-----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=1635888438.0 acme-1.21.0/tests/testdata/csr-idnsans.pem0000644000076500000240000000323300000000000017054 0ustar00bmwstaff-----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=1635888438.0 acme-1.21.0/tests/testdata/csr-ipsans.pem0000644000076500000240000000163000000000000016711 0ustar00bmwstaff-----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=1635888438.0 acme-1.21.0/tests/testdata/csr-ipv6sans.pem0000644000076500000240000000167000000000000017171 0ustar00bmwstaff-----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=1635888438.0 acme-1.21.0/tests/testdata/csr-mixed.pem0000644000076500000240000000164400000000000016527 0ustar00bmwstaff-----BEGIN CERTIFICATE REQUEST----- MIICdjCCAV4CAQIwADCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMXq v1y8EIcCbaUIzCtOcLkLS0MJ35oS+6DmV5WB1A0cIk6YrjsHIsY2lwMm13BWIvmw tY+Y6n0rr7eViNx5ZRGHpHEI/TL3Neb+VefTydL5CgvK3dd4ex2kSbTaed3fmpOx qMajEduwNcZPCcmoEXPkfrCP8w2vKQUkQ+JRPcdX1nTuzticeRP5B7YCmJsmxkEh Y0tzzZ+NIRDARoYNofefY86h3e5q66gtJxccNchmIM3YQahhg5n3Xoo8hGfM/TIc R7ncCBCLO6vtqo0QFva/NQODrgOmOsmgvqPkUWQFdZfWM8yIaU826dktx0CPB78t TudnJ1rBRvGsjHMsZikCAwEAAaAxMC8GCSqGSIb3DQEJDjEiMCAwHgYDVR0RBBcw FYINYS5leGVtcGxlLmNvbYcEwAACbzANBgkqhkiG9w0BAQsFAAOCAQEAdGMcRCxq 1X09gn1TNdMt64XUv+wdJCKDaJ+AgyIJj7QvVw8H5k7dOnxS4I+a/yo4jE+LDl2/ AuHcBLFEI4ddewdJSMrTNZjuRYuOdr3KP7fL7MffICSBi45vw5EOXg0tnjJCEiKu 6gcJgbLSP5JMMd7Haf33Q/VWsmHofR3VwOMdrnakwAU3Ff5WTuXTNVhL1kT/uLFX yW1ru6BF4unwNqSR2UeulljpNfRBsiN4zJK11W6n9KT0NkBr9zY5WCM4sW7i8k9V TeypWGo3jBKzYAGeuxZsB97U77jZ2lrGdBLZKfbcjnTeRVqCvCRrui4El7UGYFmj 7s6OJyWx5DSV8w== -----END CERTIFICATE REQUEST----- ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1635888438.0 acme-1.21.0/tests/testdata/csr-nosans.pem0000644000076500000240000000070400000000000016716 0ustar00bmwstaff-----BEGIN CERTIFICATE REQUEST----- MIIBFTCBwAIBADBbMQswCQYDVQQGEwJBVTETMBEGA1UECAwKU29tZS1TdGF0ZTEh MB8GA1UECgwYSW50ZXJuZXQgV2lkZ2l0cyBQdHkgTHRkMRQwEgYDVQQDDAtleGFt cGxlLm9yZzBcMA0GCSqGSIb3DQEBAQUAA0sAMEgCQQD0thFxUTc2v6qV55wRxfwn BUOeN4bVfu5ywJqy65kzR7T1yZi5TPEiQyM7/3HgBVy9ddFc8RX4vNZaR+ROXNEz AgMBAAGgADANBgkqhkiG9w0BAQsFAANBAMikGL8Ch7hQCStXH7chhDp6+pt2+VSo wgsrPQ2Bw4veDMlSemUrH+4e0TwbbntHfvXTDHWs9P3BiIDJLxFrjuA= -----END CERTIFICATE REQUEST----- ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1635888438.0 acme-1.21.0/tests/testdata/csr-san.pem0000644000076500000240000000107600000000000016201 0ustar00bmwstaff-----BEGIN CERTIFICATE REQUEST----- MIIBbjCCARgCAQAweTELMAkGA1UEBhMCVVMxETAPBgNVBAgMCE1pY2hpZ2FuMRIw EAYDVQQHDAlBbm4gQXJib3IxDDAKBgNVBAoMA0VGRjEfMB0GA1UECwwWVW5pdmVy c2l0eSBvZiBNaWNoaWdhbjEUMBIGA1UEAwwLZXhhbXBsZS5jb20wXDANBgkqhkiG 9w0BAQEFAANLADBIAkEArHVztFHtH92ucFJD/N/HW9AsdRsUuHUBBBDlHwNlRd3f p580rv2+6QWE30cWgdmJS86ObRz6lUTor4R0T+3C5QIDAQABoDowOAYJKoZIhvcN AQkOMSswKTAnBgNVHREEIDAeggtleGFtcGxlLmNvbYIPd3d3LmV4YW1wbGUuY29t MA0GCSqGSIb3DQEBCwUAA0EAZGBM8J1rRs7onFgtc76mOeoT1c3v0ZsEmxQfb2Wy tmReY6X1N4cs38D9VSow+VMRu2LWkKvzS7RUFSaTaeQz1A== -----END CERTIFICATE REQUEST----- ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1635888438.0 acme-1.21.0/tests/testdata/csr.der0000644000076500000240000000113700000000000015411 0ustar00bmwstaff0[0C010U example.com0"0  *H 0 |0w3$Gav1.k>=ae\u[DnXߋ":ājP]teMp*8(if3-/e\vui DKlK˷ nal3g3$aLOGL) 3S񪓪̀,˞j s|H"0yc_M=.ۢh5JIh^+WɯFWs;?Gs9_̆eƞT4)k0  *H  r0J0*;s k[-I%I!6gu%i8O}D6>:}_QMKYB4a861p @TEXUBï Pr>h6(.Μhw ٖqaQ_PkN=EКS?E %OdѻK=.gh첶Be+_)uUusZQÍV8s0Qr;`z=FI././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1635888438.0 acme-1.21.0/tests/testdata/csr.pem0000644000076500000240000000104600000000000015417 0ustar00bmwstaff-----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=1635888438.0 acme-1.21.0/tests/testdata/dsa512_key.pem0000644000076500000240000000125400000000000016500 0ustar00bmwstaff-----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=1635888438.0 acme-1.21.0/tests/testdata/rsa1024_cert.pem0000644000076500000240000000135500000000000016744 0ustar00bmwstaff-----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=1635888438.0 acme-1.21.0/tests/testdata/rsa1024_key.pem0000644000076500000240000000156700000000000016604 0ustar00bmwstaff-----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=1635888438.0 acme-1.21.0/tests/testdata/rsa2048_cert.pem0000644000076500000240000000241600000000000016752 0ustar00bmwstaff-----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=1635888438.0 acme-1.21.0/tests/testdata/rsa2048_key.pem0000644000076500000240000000325000000000000016602 0ustar00bmwstaff-----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=1635888438.0 acme-1.21.0/tests/testdata/rsa256_key.pem0000644000076500000240000000045200000000000016522 0ustar00bmwstaff-----BEGIN RSA PRIVATE KEY----- MIGrAgEAAiEAm2Fylv+Uz7trgTW8EBHP3FQSMeZs2GNQ6VRo1sIVJEkCAwEAAQIh AJT0BA/xD01dFCAXzSNyj9nfSZa3NpqzJZZn/eOm7vghAhEAzUVNZn4lLLBD1R6N E8TKNQIRAMHHyn3O5JeY36lwKwkUlEUCEAliRauN0L0+QZuYjfJ9aJECEGx4dru3 rTPCyighdqWNlHUCEQCiLjlwSRtWgmMBudCkVjzt -----END RSA PRIVATE KEY----- ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1635888438.0 acme-1.21.0/tests/testdata/rsa4096_cert.pem0000644000076500000240000000342600000000000016761 0ustar00bmwstaff-----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=1635888438.0 acme-1.21.0/tests/testdata/rsa4096_key.pem0000644000076500000240000000625700000000000016621 0ustar00bmwstaff-----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=1635888438.0 acme-1.21.0/tests/testdata/rsa512_key.pem0000644000076500000240000000075500000000000016523 0ustar00bmwstaff-----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=1635888438.0 acme-1.21.0/tests/util_test.py0000644000076500000240000000071000000000000014677 0ustar00bmwstaff"""Tests for acme.util.""" import unittest class MapKeysTest(unittest.TestCase): """Tests for acme.util.map_keys.""" def test_it(self): from acme.util import map_keys self.assertEqual({'a': 'b', 'c': 'd'}, map_keys({'a': 'b', 'c': 'd'}, lambda key: key)) self.assertEqual({2: 2, 4: 4}, map_keys({1: 2, 3: 4}, lambda x: x + 1)) if __name__ == '__main__': unittest.main() # pragma: no cover