Script 'mail_helper' called by obssrc Hello community, here is the log from the commit of package python-acme for openSUSE:Factory checked in at 2022-04-08 22:46:00 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Comparing /work/SRC/openSUSE:Factory/python-acme (Old) and /work/SRC/openSUSE:Factory/.python-acme.new.1900 (New) ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "python-acme" Fri Apr 8 22:46:00 2022 rev:57 rq:967759 version:1.26.0 Changes: -------- --- /work/SRC/openSUSE:Factory/python-acme/python-acme.changes 2021-12-21 20:16:45.257152683 +0100 +++ /work/SRC/openSUSE:Factory/.python-acme.new.1900/python-acme.changes 2022-04-08 22:46:16.478768940 +0200 @@ -1,0 +2,19 @@ +Thu Apr 7 15:33:21 UTC 2022 - Mark??ta Machov?? <mmach...@suse.com> + +- Update to version 1.26.0 + * Added show_account subcommand, which will fetch the account information from + the ACME server and show the account details (account URL and, if applicable, + email address or addresses) + * The acme library now requires requests>=2.20.0. + * Certbot and its acme library now require pytz>=2019.3. + * Certbot and its acme module now depend on josepy>=1.13.0 due to better type annotation support. + * Previously, when Certbot was in the process of registering a new ACME account + and the ACME server did not present any Terms of Service, the user was asked + to agree with a non-existent Terms of Service ("None"). This bug is now fixed, + so that if an ACME server does not provide any Terms of Service to agree with, + the user is not asked to agree to a non-existent Terms of Service any longer. + * If account registration fails, Certbot did not relay the error from the ACME + server back to the user. This is now fixed: the error message from the ACME + server is now presented to the user when account registration fails. + +------------------------------------------------------------------- Old: ---- acme-1.22.0.tar.gz acme-1.22.0.tar.gz.asc New: ---- acme-1.26.0.tar.gz acme-1.26.0.tar.gz.asc ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Other differences: ------------------ ++++++ python-acme.spec ++++++ --- /var/tmp/diff_new_pack.JRAhEi/_old 2022-04-08 22:46:17.050762592 +0200 +++ /var/tmp/diff_new_pack.JRAhEi/_new 2022-04-08 22:46:17.054762547 +0200 @@ -1,7 +1,7 @@ # # spec file # -# Copyright (c) 2021 SUSE LLC +# Copyright (c) 2022 SUSE LLC # # All modifications and additions to the file contributed by third parties # remain the property of their copyright owners, unless otherwise agreed @@ -20,7 +20,7 @@ %define skip_python2 1 %define libname acme Name: python-%{libname} -Version: 1.22.0 +Version: 1.26.0 Release: 0 Summary: Python library for the ACME protocol License: Apache-2.0 @@ -29,22 +29,22 @@ Source1: https://files.pythonhosted.org/packages/source/a/%{libname}/%{libname}-%{version}.tar.gz.asc Source2: %{name}.keyring BuildRequires: %{python_module cryptography >= 2.5.0} -BuildRequires: %{python_module josepy >= 1.9.0} +BuildRequires: %{python_module josepy >= 1.13.0} BuildRequires: %{python_module pyOpenSSL >= 17.3.0} BuildRequires: %{python_module pyRFC3339} BuildRequires: %{python_module pytest} -BuildRequires: %{python_module pytz} -BuildRequires: %{python_module requests >= 2.14.2} +BuildRequires: %{python_module pytz >= 2019.3} +BuildRequires: %{python_module requests >= 2.20.0} BuildRequires: %{python_module requests-toolbelt >= 0.3.0} BuildRequires: %{python_module setuptools} BuildRequires: fdupes BuildRequires: python-rpm-macros Requires: python-cryptography >= 2.5.0 -Requires: python-josepy >= 1.9.0 +Requires: python-josepy >= 1.13.0 Requires: python-pyOpenSSL >= 17.3.0 Requires: python-pyRFC3339 -Requires: python-pytz -Requires: python-requests >= 2.14.2 +Requires: python-pytz >= 2019.3 +Requires: python-requests >= 2.20.0 Requires: python-requests-toolbelt >= 0.3.0 BuildArch: noarch %if %{?suse_version} < 1500 ++++++ acme-1.22.0.tar.gz -> acme-1.26.0.tar.gz ++++++ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/acme-1.22.0/MANIFEST.in new/acme-1.26.0/MANIFEST.in --- old/acme-1.22.0/MANIFEST.in 2021-12-07 23:02:45.000000000 +0100 +++ new/acme-1.26.0/MANIFEST.in 2022-04-05 19:41:26.000000000 +0200 @@ -4,5 +4,6 @@ recursive-include docs * recursive-include examples * recursive-include tests * +include acme/py.typed global-exclude __pycache__ global-exclude *.py[cod] diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/acme-1.22.0/PKG-INFO new/acme-1.26.0/PKG-INFO --- old/acme-1.22.0/PKG-INFO 2021-12-07 23:02:52.134568200 +0100 +++ new/acme-1.26.0/PKG-INFO 2022-04-05 19:41:33.131234600 +0200 @@ -1,6 +1,6 @@ Metadata-Version: 2.1 Name: acme -Version: 1.22.0 +Version: 1.26.0 Summary: ACME protocol implementation in Python Home-page: https://github.com/letsencrypt/letsencrypt Author: Certbot Project @@ -12,14 +12,13 @@ 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: Programming Language :: Python :: 3.10 Classifier: Topic :: Internet :: WWW/HTTP Classifier: Topic :: Security -Requires-Python: >=3.6 +Requires-Python: >=3.7 Provides-Extra: docs Provides-Extra: test License-File: LICENSE.txt diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/acme-1.22.0/acme/__init__.py new/acme-1.26.0/acme/__init__.py --- old/acme-1.22.0/acme/__init__.py 2021-12-07 23:02:45.000000000 +0100 +++ new/acme-1.26.0/acme/__init__.py 2022-04-05 19:41:26.000000000 +0200 @@ -2,7 +2,7 @@ This module is an implementation of the `ACME protocol`_. -.. _`ACME protocol`: https://ietf-wg-acme.github.io/acme +.. _`ACME protocol`: https://datatracker.ietf.org/doc/html/rfc8555 """ import sys diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/acme-1.22.0/acme/challenges.py new/acme-1.26.0/acme/challenges.py --- old/acme-1.22.0/acme/challenges.py 2021-12-07 23:02:45.000000000 +0100 +++ new/acme-1.26.0/acme/challenges.py 2022-04-05 19:41:26.000000000 +0200 @@ -12,6 +12,8 @@ from typing import Optional from typing import Tuple from typing import Type +from typing import TypeVar +from typing import Union from cryptography.hazmat.primitives import hashes import josepy as jose @@ -27,6 +29,8 @@ logger = logging.getLogger(__name__) +GenericChallenge = TypeVar('GenericChallenge', bound='Challenge') + class Challenge(jose.TypedJSONObjectWithFields): # _fields_to_partial_json @@ -34,9 +38,10 @@ TYPES: Dict[str, Type['Challenge']] = {} @classmethod - def from_json(cls, jobj: Mapping[str, Any]) -> 'Challenge': + def from_json(cls: Type[GenericChallenge], + jobj: Mapping[str, Any]) -> Union[GenericChallenge, 'UnrecognizedChallenge']: try: - return super().from_json(jobj) + return cast(GenericChallenge, super().from_json(jobj)) except jose.UnrecognizedTypeError as error: logger.debug(error) return UnrecognizedChallenge.from_json(jobj) @@ -47,7 +52,7 @@ """ACME challenge response.""" TYPES: Dict[str, Type['ChallengeResponse']] = {} resource_type = 'challenge' - resource = fields.Resource(resource_type) + resource: str = fields.resource(resource_type) class UnrecognizedChallenge(Challenge): @@ -62,6 +67,7 @@ :ivar jobj: Original JSON decoded object. """ + jobj: Dict[str, Any] def __init__(self, jobj: Mapping[str, Any]) -> None: super().__init__() @@ -85,7 +91,7 @@ """Minimum size of the :attr:`token` in bytes.""" # TODO: acme-spec doesn't specify token as base64-encoded value - token: bytes = jose.Field( + token: bytes = jose.field( "token", encoder=jose.encode_b64jose, decoder=functools.partial( jose.decode_b64jose, size=TOKEN_SIZE, minimum=True)) @@ -108,10 +114,10 @@ class KeyAuthorizationChallengeResponse(ChallengeResponse): """Response to Challenges based on Key Authorization. - :param unicode key_authorization: + :param str key_authorization: """ - key_authorization = jose.Field("keyAuthorization") + key_authorization: str = jose.field("keyAuthorization") thumbprint_hash_function = hashes.SHA256 def verify(self, chall: 'KeyAuthorizationChallenge', account_public_key: jose.JWK) -> bool: @@ -126,7 +132,7 @@ :rtype: bool """ - parts = self.key_authorization.split('.') + parts = self.key_authorization.split('.') # pylint: disable=no-member if len(parts) != 2: logger.debug("Key authorization (%r) is not well formed", self.key_authorization) @@ -152,6 +158,9 @@ return jobj +# TODO: Make this method a generic of K (bound=KeyAuthorizationChallenge), response_cls of type +# Type[K] and use it in response/response_and_validation return types once Python 3.6 support is +# dropped (do not support generic ABC classes, see https://github.com/python/typing/issues/449). class KeyAuthorizationChallenge(_TokenChallenge, metaclass=abc.ABCMeta): """Challenge based on Key Authorization. @@ -168,7 +177,7 @@ """Generate Key Authorization. :param JWK account_key: - :rtype unicode: + :rtype str: """ return self.encode("token") + "." + jose.b64encode( @@ -229,7 +238,7 @@ around `KeyAuthorizationChallengeResponse.verify`. :param challenges.DNS01 chall: Corresponding challenge. - :param unicode domain: Domain name being verified. + :param str domain: Domain name being verified. :param JWK account_public_key: Public key for the key pair being authorized. @@ -257,7 +266,7 @@ """Generate validation. :param JWK account_key: - :rtype: unicode + :rtype: str """ return jose.b64encode(hashlib.sha256(self.key_authorization( @@ -266,10 +275,11 @@ def validation_domain_name(self, name: str) -> str: """Domain name for TXT validation record. - :param unicode name: Domain name being validated. + :param str name: Domain name being validated. + :rtype: str """ - return "{0}.{1}".format(self.LABEL, name) + return f"{self.LABEL}.{name}" @ChallengeResponse.register @@ -293,7 +303,7 @@ """Simple verify. :param challenges.SimpleHTTP chall: Corresponding challenge. - :param unicode domain: Domain name being verified. + :param str domain: Domain name being verified. :param JWK account_public_key: Public key for the key pair being authorized. :param int port: Port used in the validation. @@ -357,7 +367,7 @@ def path(self) -> str: """Path (starting with '/') for provisioned resource. - :rtype: string + :rtype: str """ return '/' + self.URI_ROOT_PATH + '/' + self.encode('token') @@ -368,8 +378,8 @@ Forms an URI to the HTTPS server provisioned resource (containing :attr:`~SimpleHTTP.token`). - :param unicode domain: Domain name being verified. - :rtype: string + :param str domain: Domain name being verified. + :rtype: str """ return "http://" + domain + self.path @@ -378,7 +388,7 @@ """Generate validation. :param JWK account_key: - :rtype: unicode + :rtype: str """ return self.key_authorization(account_key) @@ -409,7 +419,7 @@ ) -> Tuple[crypto.X509, crypto.PKey]: """Generate tls-alpn-01 certificate. - :param unicode domain: Domain verified by the challenge. + :param str domain: Domain verified by the challenge. :param OpenSSL.crypto.PKey key: Optional private key used in certificate generation. If not provided (``None``), then fresh key will be generated. @@ -433,8 +443,8 @@ 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 str domain: domain being validated, required. + :param str host: IP address used to probe the certificate. :param int port: Port used to probe the certificate. """ @@ -450,7 +460,7 @@ def verify_cert(self, domain: str, cert: crypto.X509) -> bool: """Verify tls-alpn-01 challenge certificate. - :param unicode domain: Domain name being validated. + :param str domain: Domain name being validated. :param OpensSSL.crypto.X509 cert: Challenge certificate. :returns: Whether the certificate was successfully verified. @@ -462,7 +472,7 @@ # 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] + cert.digest('sha256'), names) if len(names) != 1 or names[0].lower() != domain.lower(): return False @@ -523,7 +533,7 @@ """Generate validation. :param JWK account_key: - :param unicode domain: Domain verified by the challenge. + :param str domain: Domain verified by the challenge. :param OpenSSL.crypto.PKey cert_key: Optional private key used in certificate generation. If not provided (``None``), then fresh key will be generated. @@ -531,9 +541,10 @@ :rtype: `tuple` of `OpenSSL.crypto.X509` and `OpenSSL.crypto.PKey` """ - return self.response(account_key).gen_cert( + # TODO: Remove cast when response() is generic. + return cast(TLSALPN01Response, self.response(account_key)).gen_cert( key=kwargs.get('cert_key'), - domain=kwargs.get('domain')) + domain=cast(str, kwargs.get('domain'))) @staticmethod def is_supported() -> bool: @@ -599,13 +610,12 @@ :rtype: DNSResponse """ - return DNSResponse(validation=self.gen_validation( - account_key, **kwargs)) + 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. + :param str name: Domain name being validated. """ return "{0}.{1}".format(self.LABEL, name) @@ -620,7 +630,7 @@ """ typ = "dns" - validation = jose.Field("validation", decoder=jose.JWS.from_json) + validation: jose.JWS = jose.field("validation", decoder=jose.JWS.from_json) def check_validation(self, chall: 'DNS', account_public_key: jose.JWK) -> bool: """Check validation. @@ -631,4 +641,4 @@ :rtype: bool """ - return chall.check_validation(cast(jose.JWS, self.validation), account_public_key) + return chall.check_validation(self.validation, account_public_key) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/acme-1.22.0/acme/client.py new/acme-1.26.0/acme/client.py --- old/acme-1.22.0/acme/client.py 2021-12-07 23:02:45.000000000 +0100 +++ new/acme-1.26.0/acme/client.py 2022-04-05 19:41:26.000000000 +0200 @@ -19,6 +19,7 @@ from typing import Dict from typing import Iterable from typing import List +from typing import Mapping from typing import Optional from typing import Set from typing import Text @@ -33,6 +34,7 @@ from requests.utils import parse_header_links from requests_toolbelt.adapters.source import SourceAddressAdapter +from acme import challenges from acme import crypto_util from acme import errors from acme import jws @@ -156,12 +158,12 @@ 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: + if identifier is not None and authzr.body.identifier != identifier: # pylint: disable=no-member raise errors.UnexpectedUpdate(authzr) return authzr - def answer_challenge(self, challb: messages.ChallengeBody, response: requests.Response - ) -> messages.ChallengeResource: + def answer_challenge(self, challb: messages.ChallengeBody, + response: challenges.ChallengeResponse) -> messages.ChallengeResource: """Answer challenge. :param challb: Challenge Resource body. @@ -176,15 +178,15 @@ :raises .UnexpectedUpdate: """ - response = self._post(challb.uri, response) + resp = self._post(challb.uri, response) try: - authzr_uri = response.links['up']['url'] + authzr_uri = resp.links['up']['url'] except KeyError: raise errors.ClientError('"up" Link header missing') challr = messages.ChallengeResource( authzr_uri=authzr_uri, - body=messages.ChallengeBody.from_json(response.json())) - # TODO: check that challr.uri == response.headers['Location']? + body=messages.ChallengeBody.from_json(resp.json())) + # TODO: check that challr.uri == resp.headers['Location']? if challr.uri != challb.uri: raise errors.UnexpectedUpdate(challr.uri) return challr @@ -492,7 +494,7 @@ updated[authzr] = updated_authzr attempts[authzr] += 1 - if updated_authzr.body.status not in ( + if updated_authzr.body.status not in ( # pylint: disable=no-member messages.STATUS_VALID, messages.STATUS_INVALID): if attempts[authzr] < max_attempts: # push back to the priority queue, with updated retry_after @@ -599,7 +601,7 @@ :raises .ClientError: If revocation is unsuccessful. """ - self._revoke(cert, rsn, self.directory[cast(str, messages.Revocation)]) + self._revoke(cert, rsn, self.directory[messages.Revocation]) class ClientV2(ClientBase): @@ -756,7 +758,7 @@ 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: + if authzr.body.status != messages.STATUS_PENDING: # pylint: disable=no-member responses.append(authzr) break time.sleep(1) @@ -897,14 +899,15 @@ 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 + regr_res = client_v1.register(regr) + if regr_res.terms_of_service is not None: + _assess_tos(regr_res.terms_of_service) + return client_v1.agree_to_tos(regr_res) + return regr_res else: client_v2 = cast(ClientV2, self.client) - if "terms_of_service" in client_v2.directory.meta: + if ("terms_of_service" in client_v2.directory.meta and + client_v2.directory.meta.terms_of_service is not None): _assess_tos(client_v2.directory.meta.terms_of_service) regr = regr.update(terms_of_service_agreed=True) return client_v2.new_account(regr) @@ -970,7 +973,8 @@ 'certificate, please rerun the command for a new one.') cert = OpenSSL.crypto.dump_certificate( - OpenSSL.crypto.FILETYPE_PEM, certr.body.wrapped).decode() + OpenSSL.crypto.FILETYPE_PEM, + cast(OpenSSL.crypto.X509, cast(jose.ComparableX509, certr.body).wrapped)).decode() chain_str = crypto_util.dump_pyopenssl_chain(chain).decode() return orderr.update(fullchain_pem=(cert + chain_str)) @@ -1056,7 +1060,7 @@ pass def _wrap_in_jws(self, obj: jose.JSONDeSerializable, nonce: str, url: str, - acme_version: int) -> jose.JWS: + acme_version: int) -> str: """Wrap `JSONDeSerializable` object in JWS. .. todo:: Implement ``acmePath``. @@ -1064,7 +1068,7 @@ :param josepy.JSONDeSerializable obj: :param str url: The URL to which this object will be POSTed :param str nonce: - :rtype: `josepy.JWS` + :rtype: str """ if isinstance(obj, VersionedLEACMEMixin): @@ -1073,7 +1077,7 @@ logger.debug('JWS payload:\n%s', jobj) kwargs = { "alg": self.alg, - "nonce": nonce + "nonce": nonce, } if acme_version == 2: kwargs["url"] = url @@ -1082,7 +1086,7 @@ 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) + return jws.JWS.sign(jobj, **cast(Mapping[str, Any], kwargs)).json_dumps(indent=2) @classmethod def _check_response(cls, response: requests.Response, @@ -1100,7 +1104,7 @@ is ignored, but logged. :raises .messages.Error: If server response body - carries HTTP Problem (draft-ietf-appsawg-http-problem-00). + carries HTTP Problem (https://datatracker.ietf.org/doc/html/rfc7807). :raises .ClientError: In case of other networking errors. """ @@ -1139,8 +1143,7 @@ '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)) + raise errors.ClientError(f'Unexpected response Content-Type: {response_ct}') return response @@ -1193,7 +1196,7 @@ 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)) + raise ValueError(f"Requesting {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 diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/acme-1.22.0/acme/crypto_util.py new/acme-1.26.0/acme/crypto_util.py --- old/acme-1.22.0/acme/crypto_util.py 2021-12-07 23:02:45.000000000 +0100 +++ new/acme-1.26.0/acme/crypto_util.py 2022-04-05 19:41:26.000000000 +0200 @@ -278,7 +278,7 @@ :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` + :rtype: `list` of `str` """ # This function finds SANs with dns name @@ -300,7 +300,7 @@ :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 + :rtype: `list` of `str`. note that this returns as string, not IPaddress object """ @@ -320,7 +320,7 @@ :type cert_or_req: `OpenSSL.crypto.X509` or `OpenSSL.crypto.X509Req`. :returns: raw san strings, parsed byte as utf-8 - :rtype: `list` of `unicode` + :rtype: `list` of `str` """ # This function finds SANs by dumping the certificate/CSR to text and @@ -352,7 +352,7 @@ ) -> crypto.X509: """Generate new self-signed certificate. - :type domains: `list` of `unicode` + :type domains: `list` of `str` :param OpenSSL.crypto.PKey key: :param bool force_san: :param extensions: List of additional extensions to include in the cert. @@ -410,7 +410,8 @@ return cert -def dump_pyopenssl_chain(chain: List[crypto.X509], filetype: int = crypto.FILETYPE_PEM) -> bytes: +def dump_pyopenssl_chain(chain: Union[List[jose.ComparableX509], 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 @@ -425,6 +426,8 @@ def _dump_cert(cert: Union[jose.ComparableX509, crypto.X509]) -> bytes: if isinstance(cert, jose.ComparableX509): + if isinstance(cert.wrapped, crypto.X509Req): + raise errors.Error("Unexpected CSR provided.") # pragma: no cover cert = cert.wrapped return crypto.dump_certificate(filetype, cert) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/acme-1.22.0/acme/fields.py new/acme-1.26.0/acme/fields.py --- old/acme-1.22.0/acme/fields.py 2021-12-07 23:02:45.000000000 +0100 +++ new/acme-1.26.0/acme/fields.py 2022-04-05 19:41:26.000000000 +0200 @@ -56,8 +56,8 @@ def __init__(self, resource_type: str, *args: Any, **kwargs: Any) -> None: self.resource_type = resource_type - super().__init__( - 'resource', default=resource_type, *args, **kwargs) + kwargs['default'] = resource_type + super().__init__('resource', *args, **kwargs) def decode(self, value: Any) -> Any: if value != self.resource_type: @@ -65,3 +65,18 @@ 'Wrong resource type: {0} instead of {1}'.format( value, self.resource_type)) return value + + +def fixed(json_name: str, value: Any) -> Any: + """Generates a type-friendly Fixed field.""" + return Fixed(json_name, value) + + +def rfc3339(json_name: str, omitempty: bool = False) -> Any: + """Generates a type-friendly RFC3339 field.""" + return RFC3339Field(json_name, omitempty=omitempty) + + +def resource(resource_type: str) -> Any: + """Generates a type-friendly Resource field.""" + return Resource(resource_type) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/acme-1.22.0/acme/jws.py new/acme-1.26.0/acme/jws.py --- old/acme-1.22.0/acme/jws.py 2021-12-07 23:02:45.000000000 +0100 +++ new/acme-1.26.0/acme/jws.py 2022-04-05 19:41:26.000000000 +0200 @@ -12,14 +12,14 @@ 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) + nonce: Optional[bytes] = jose.field('nonce', omitempty=True, encoder=jose.encode_b64jose) + kid: Optional[str] = jose.field('kid', omitempty=True) + url: Optional[str] = jose.field('url', omitempty=True) # Mypy does not understand the josepy magic happening here, and falsely claims # that nonce is redefined. Let's ignore the type check here. - @nonce.decoder # type: ignore - def nonce(value: str) -> bytes: # pylint: disable=no-self-argument,missing-function-docstring + @nonce.decoder # type: ignore[no-redef,union-attr] + def nonce(value: str) -> bytes: # type: ignore[misc] # pylint: disable=no-self-argument,missing-function-docstring try: return jose.decode_b64jose(value) except jose.DeserializationError as error: @@ -29,12 +29,12 @@ class Signature(jose.Signature): """ACME-specific Signature. Uses ACME-specific Header for customer fields.""" - __slots__ = jose.Signature._orig_slots # pylint: disable=no-member + __slots__ = jose.Signature._orig_slots # type: ignore[attr-defined] # pylint: disable=protected-access,no-member # TODO: decoder/encoder should accept cls? Otherwise, subclassing # JSONObjectWithFields is tricky... header_cls = Header - header = jose.Field( + header: Header = jose.field( 'header', omitempty=True, default=header_cls(), decoder=header_cls.from_json) @@ -44,10 +44,10 @@ class JWS(jose.JWS): """ACME-specific JWS. Includes none, url, and kid in protected header.""" signature_cls = Signature - __slots__ = jose.JWS._orig_slots + __slots__ = jose.JWS._orig_slots # type: ignore[attr-defined] # pylint: disable=protected-access @classmethod - # pylint: disable=arguments-differ + # type: ignore[override] # pylint: disable=arguments-differ def sign(cls, payload: bytes, key: jose.JWK, alg: jose.JWASignature, nonce: Optional[bytes], url: Optional[str] = None, kid: Optional[str] = None) -> jose.JWS: # Per ACME spec, jwk and kid are mutually exclusive, so only include a diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/acme-1.22.0/acme/messages.py new/acme-1.26.0/acme/messages.py --- old/acme-1.22.0/acme/messages.py 2021-12-07 23:02:45.000000000 +0100 +++ new/acme-1.26.0/acme/messages.py 2022-04-05 19:41:26.000000000 +0200 @@ -1,4 +1,5 @@ """ACME protocol messages.""" +import datetime from collections.abc import Hashable import json from typing import Any @@ -7,9 +8,12 @@ from typing import List from typing import Mapping from typing import MutableMapping +from typing import Optional from typing import Tuple from typing import Type -from typing import Optional +from typing import TYPE_CHECKING +from typing import TypeVar +from typing import Union import josepy as jose @@ -20,6 +24,11 @@ from acme import util from acme.mixins import ResourceMixin +if TYPE_CHECKING: + from typing_extensions import Protocol # pragma: no cover +else: + Protocol = object + OLD_ERROR_PREFIX = "urn:acme:error:" ERROR_PREFIX = "urn:ietf:params:acme:error:" @@ -56,11 +65,11 @@ '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())) +ERROR_TYPE_DESCRIPTIONS = {**{ + ERROR_PREFIX + name: desc for name, desc in ERROR_CODES.items() +}, **{ # 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: @@ -73,22 +82,22 @@ class Error(jose.JSONObjectWithFields, errors.Error): """ACME error. - https://tools.ietf.org/html/draft-ietf-appsawg-http-problem-00 + https://datatracker.ietf.org/doc/html/rfc7807 - :ivar unicode typ: - :ivar unicode title: - :ivar unicode detail: + :ivar str typ: + :ivar str title: + :ivar str detail: """ - typ = jose.Field('type', omitempty=True, default='about:blank') - title = jose.Field('title', omitempty=True) - detail = jose.Field('detail', omitempty=True) + typ: str = jose.field('type', omitempty=True, default='about:blank') + title: str = jose.field('title', omitempty=True) + detail: str = 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'. + :str code: An ACME error code, like 'dnssec'. :kwargs: kwargs to pass to Error. """ @@ -98,14 +107,14 @@ 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 + return cls(typ=typ, **kwargs) @property def description(self) -> Optional[str]: """Hardcoded error description based on its type. :returns: Description if standard ACME error or ``None``. - :rtype: unicode + :rtype: str """ return ERROR_TYPE_DESCRIPTIONS.get(self.typ) @@ -117,7 +126,7 @@ Basically self.typ without the ERROR_PREFIX. :returns: error code if standard ACME code or ``None``. - :rtype: unicode + :rtype: str """ code = str(self.typ).rsplit(':', maxsplit=1)[-1] @@ -148,12 +157,11 @@ @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__)) + raise jose.DeserializationError(f'{cls.__name__} not recognized') return cls.POSSIBLE_NAMES[jobj] def __repr__(self) -> str: - return '{0}({1})'.format(self.__class__.__name__, self.name) + return f'{self.__class__.__name__}({self.name})' def __eq__(self, other: Any) -> bool: return isinstance(other, type(self)) and other.name == self.name @@ -164,7 +172,7 @@ class Status(_Constant): """ACME "status" field.""" - POSSIBLE_NAMES: Dict[str, 'Status'] = {} + POSSIBLE_NAMES: Dict[str, _Constant] = {} STATUS_UNKNOWN = Status('unknown') @@ -179,7 +187,7 @@ class IdentifierType(_Constant): """ACME identifier type.""" - POSSIBLE_NAMES: Dict[str, 'IdentifierType'] = {} + POSSIBLE_NAMES: Dict[str, _Constant] = {} IDENTIFIER_FQDN = IdentifierType('dns') # IdentifierDNS in Boulder @@ -190,25 +198,35 @@ """ACME identifier. :ivar IdentifierType typ: - :ivar unicode value: + :ivar str value: """ - typ = jose.Field('type', decoder=IdentifierType.from_json) - value = jose.Field('value') + typ: IdentifierType = jose.field('type', decoder=IdentifierType.from_json) + value: str = jose.field('value') + + +class HasResourceType(Protocol): + """ + Represents a class with a resource_type class parameter of type string. + """ + resource_type: str = NotImplemented + + +GenericHasResourceType = TypeVar("GenericHasResourceType", bound=HasResourceType) class Directory(jose.JSONDeSerializable): """Directory.""" - _REGISTERED_TYPES: Dict[str, Type['Directory']] = {} + _REGISTERED_TYPES: Dict[str, Type[HasResourceType]] = {} 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) + _terms_of_service: str = jose.field('terms-of-service', omitempty=True) + _terms_of_service_v2: str = jose.field('termsOfService', omitempty=True) + website: str = jose.field('website', omitempty=True) + caa_identities: List[str] = jose.field('caaIdentities', omitempty=True) + external_account_required: bool = jose.field('externalAccountRequired', omitempty=True) def __init__(self, **kwargs: Any) -> None: kwargs = {self._internal_name(k): v for k, v in kwargs.items()} @@ -229,11 +247,14 @@ return '_' + name if name == 'terms_of_service' else name @classmethod - def _canon_key(cls, key: str) -> str: - return getattr(key, 'resource_type', key) + def _canon_key(cls, key: Union[str, HasResourceType, Type[HasResourceType]]) -> str: + if isinstance(key, str): + return key + return key.resource_type @classmethod - def register(cls, resource_body_cls: Type['Directory']) -> Type['Directory']: + def register(cls, + resource_body_cls: Type[GenericHasResourceType]) -> Type[GenericHasResourceType]: """Register resource.""" resource_type = resource_body_cls.resource_type assert resource_type not in cls._REGISTERED_TYPES @@ -252,7 +273,7 @@ except KeyError as error: raise AttributeError(str(error)) - def __getitem__(self, name: str) -> Any: + def __getitem__(self, name: Union[str, HasResourceType, Type[HasResourceType]]) -> Any: try: return self._jobj[self._canon_key(name)] except KeyError: @@ -273,16 +294,16 @@ :ivar acme.messages.ResourceBody body: Resource body. """ - body = jose.Field('body') + body: "ResourceBody" = jose.field('body') class ResourceWithURI(Resource): """ACME Resource with URI. - :ivar unicode ~.uri: Location of the resource. + :ivar str uri: Location of the resource. """ - uri = jose.Field('uri') # no ChallengeResource.uri + uri: str = jose.field('uri') # no ChallengeResource.uri class ResourceBody(jose.JSONObjectWithFields): @@ -308,35 +329,40 @@ return eab.to_partial_json() +GenericRegistration = TypeVar('GenericRegistration', bound='Registration') + + class Registration(ResourceBody): """Registration Resource Body. - :ivar josepy.jwk.JWK key: Public key. + :ivar jose.JWK key: Public key. :ivar tuple contact: Contact information following ACME spec, - `tuple` of `unicode`. - :ivar unicode agreement: + `tuple` of `str`. + :ivar str 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) + key: jose.JWK = jose.field('key', omitempty=True, decoder=jose.JWK.from_json) # Contact field implements special behavior to allow messages that clear existing # contacts while not expecting the `contact` field when loading from json. # This is implemented in the constructor and *_json methods. - contact = 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) + contact: Tuple[str, ...] = jose.field('contact', omitempty=True, default=()) + agreement: str = jose.field('agreement', omitempty=True) + status: Status = jose.field('status', omitempty=True) + terms_of_service_agreed: bool = jose.field('termsOfServiceAgreed', omitempty=True) + only_return_existing: bool = jose.field('onlyReturnExisting', omitempty=True) + external_account_binding: Dict[str, Any] = jose.field('externalAccountBinding', + omitempty=True) phone_prefix = 'tel:' email_prefix = 'mailto:' @classmethod - def from_data(cls, phone: Optional[str] = None, email: Optional[str] = None, + def from_data(cls: Type[GenericRegistration], phone: Optional[str] = None, + email: Optional[str] = None, external_account_binding: Optional[Dict[str, Any]] = None, - **kwargs: Any) -> 'Registration': + **kwargs: Any) -> GenericRegistration: """ Create registration resource from contact details. @@ -419,26 +445,26 @@ class NewRegistration(ResourceMixin, Registration): """New registration.""" resource_type = 'new-reg' - resource = fields.Resource(resource_type) + resource: str = fields.resource(resource_type) class UpdateRegistration(ResourceMixin, Registration): """Update registration.""" resource_type = 'reg' - resource = fields.Resource(resource_type) + resource: str = 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. + :ivar str new_authzr_uri: Deprecated. Do not use. + :ivar str 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) + body: Registration = jose.field('body', decoder=Registration.from_json) + new_authzr_uri: str = jose.field('new_authzr_uri', omitempty=True) + terms_of_service: str = jose.field('terms_of_service', omitempty=True) class ChallengeBody(ResourceBody): @@ -463,12 +489,12 @@ # 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, + _uri: str = jose.field('uri', omitempty=True, default=None) + _url: str = jose.field('url', omitempty=True, default=None) + status: 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, + validated: datetime.datetime = fields.rfc3339('validated', omitempty=True) + error: Error = jose.field('error', decoder=Error.from_json, omitempty=True, default=None) def __init__(self, **kwargs: Any) -> None: @@ -511,16 +537,16 @@ """Challenge Resource. :ivar acme.messages.ChallengeBody body: - :ivar unicode authzr_uri: URI found in the 'up' ``Link`` header. + :ivar str authzr_uri: URI found in the 'up' ``Link`` header. """ - body = jose.Field('body', decoder=ChallengeBody.from_json) - authzr_uri = jose.Field('authzr_uri') + body: ChallengeBody = jose.field('body', decoder=ChallengeBody.from_json) + authzr_uri: str = jose.field('authzr_uri') @property def uri(self) -> str: """The URL of the challenge body.""" - return self.body.uri + return self.body.uri # pylint: disable=no-member class Authorization(ResourceBody): @@ -534,26 +560,26 @@ :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) + identifier: Identifier = jose.field('identifier', decoder=Identifier.from_json, omitempty=True) + challenges: List[ChallengeBody] = jose.field('challenges', omitempty=True) + combinations: Tuple[Tuple[int, ...], ...] = jose.field('combinations', omitempty=True) - status = jose.Field('status', omitempty=True, decoder=Status.from_json) + status: Status = jose.field('status', omitempty=True, decoder=Status.from_json) # TODO: 'expires' is allowed for Authorization Resources in # general, but for Key Authorization '[t]he "expires" field MUST # be absent'... then acme-spec gives example with 'expires' # present... That's confusing! - expires = fields.RFC3339Field('expires', omitempty=True) - wildcard = jose.Field('wildcard', omitempty=True) + expires: datetime.datetime = fields.rfc3339('expires', omitempty=True) + wildcard: bool = jose.field('wildcard', omitempty=True) # Mypy does not understand the josepy magic happening here, and falsely claims # that challenge is redefined. Let's ignore the type check here. @challenges.decoder # type: ignore - def challenges(value: List[Mapping[str, Any]]) -> Tuple[ChallengeBody, ...]: # pylint: disable=no-self-argument,missing-function-docstring + def challenges(value: List[Dict[str, Any]]) -> Tuple[ChallengeBody, ...]: # type: ignore[misc] # 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], ...], ...]: + def resolved_combinations(self) -> Tuple[Tuple[ChallengeBody, ...], ...]: """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 @@ -563,37 +589,37 @@ class NewAuthorization(ResourceMixin, Authorization): """New authorization.""" resource_type = 'new-authz' - resource = fields.Resource(resource_type) + resource: str = fields.resource(resource_type) class UpdateAuthorization(ResourceMixin, Authorization): """Update authorization.""" resource_type = 'authz' - resource = fields.Resource(resource_type) + resource: str = fields.resource(resource_type) class AuthorizationResource(ResourceWithURI): """Authorization Resource. :ivar acme.messages.Authorization body: - :ivar unicode new_cert_uri: Deprecated. Do not use. + :ivar str 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) + body: Authorization = jose.field('body', decoder=Authorization.from_json) + new_cert_uri: str = jose.field('new_cert_uri', omitempty=True) @Directory.register class CertificateRequest(ResourceMixin, jose.JSONObjectWithFields): """ACME new-cert request. - :ivar josepy.util.ComparableX509 csr: + :ivar jose.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) + resource: str = fields.resource(resource_type) + csr: jose.ComparableX509 = jose.field('csr', decoder=jose.decode_csr, encoder=jose.encode_csr) class CertificateResource(ResourceWithURI): @@ -601,27 +627,27 @@ :ivar josepy.util.ComparableX509 body: `OpenSSL.crypto.X509` wrapped in `.ComparableX509` - :ivar unicode cert_chain_uri: URI found in the 'up' ``Link`` header + :ivar str 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') + cert_chain_uri: str = jose.field('cert_chain_uri') + authzrs: Tuple[AuthorizationResource, ...] = jose.field('authzrs') @Directory.register class Revocation(ResourceMixin, jose.JSONObjectWithFields): """Revocation message. - :ivar .ComparableX509 certificate: `OpenSSL.crypto.X509` wrapped in - `.ComparableX509` + :ivar jose.ComparableX509 certificate: `OpenSSL.crypto.X509` wrapped in + `jose.ComparableX509` """ resource_type = 'revoke-cert' - resource = fields.Resource(resource_type) - certificate = jose.Field( + resource: str = fields.resource(resource_type) + certificate: jose.ComparableX509 = jose.field( 'certificate', decoder=jose.decode_cert, encoder=jose.encode_cert) - reason = jose.Field('reason') + reason: int = jose.field('reason') class Order(ResourceBody): @@ -638,19 +664,18 @@ :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) + identifiers: List[Identifier] = jose.field('identifiers', omitempty=True) + status: Status = jose.field('status', decoder=Status.from_json, omitempty=True) + authorizations: List[str] = jose.field('authorizations', omitempty=True) + certificate: str = jose.field('certificate', omitempty=True) + finalize: str = jose.field('finalize', omitempty=True) + expires: datetime.datetime = fields.rfc3339('expires', omitempty=True) + error: Error = jose.field('error', omitempty=True, decoder=Error.from_json) # Mypy does not understand the josepy magic happening here, and falsely claims # that identifiers is redefined. Let's ignore the type check here. @identifiers.decoder # type: ignore - def identifiers(value: List[Mapping[str, Any]]) -> Tuple[Identifier, ...]: # pylint: disable=no-self-argument,missing-function-docstring + def identifiers(value: List[Dict[str, Any]]) -> Tuple[Identifier, ...]: # type: ignore[misc] # pylint: disable=no-self-argument,missing-function-docstring return tuple(Identifier.from_json(identifier) for identifier in value) @@ -658,7 +683,7 @@ """Order Resource. :ivar acme.messages.Order body: - :ivar str csr_pem: The CSR this Order will be finalized with. + :ivar bytes csr_pem: The CSR this Order will be finalized with. :ivar authorizations: Fully-fetched AuthorizationResource objects. :vartype authorizations: `list` of `acme.messages.AuthorizationResource` :ivar str fullchain_pem: The fetched contents of the certificate URL @@ -668,11 +693,13 @@ 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) + body: Order = jose.field('body', decoder=Order.from_json) + csr_pem: bytes = jose.field('csr_pem', omitempty=True) + authorizations: List[AuthorizationResource] = jose.field('authorizations') + fullchain_pem: str = jose.field('fullchain_pem', omitempty=True) + alternative_fullchains_pem: List[str] = jose.field('alternative_fullchains_pem', + omitempty=True) + @Directory.register class NewOrder(Order): diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/acme-1.22.0/acme/mixins.py new/acme-1.26.0/acme/mixins.py --- old/acme-1.22.0/acme/mixins.py 2021-12-07 23:02:45.000000000 +0100 +++ new/acme-1.26.0/acme/mixins.py 2022-04-05 19:41:26.000000000 +0200 @@ -65,4 +65,4 @@ jobj.pop(uncompliant_field, None) return jobj - raise AttributeError('Method {0}() is not implemented.'.format(jobj_method)) # pragma: no cover + raise AttributeError(f'Method {jobj_method}() is not implemented.') # pragma: no cover diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/acme-1.22.0/acme/standalone.py new/acme-1.26.0/acme/standalone.py --- old/acme-1.22.0/acme/standalone.py 2021-12-07 23:02:45.000000000 +0100 +++ new/acme-1.26.0/acme/standalone.py 2022-04-05 19:41:26.000000000 +0200 @@ -8,6 +8,7 @@ import socketserver import threading from typing import Any +from typing import cast from typing import List from typing import Mapping from typing import Optional @@ -34,16 +35,15 @@ else: self.address_family = socket.AF_INET self.certs = kwargs.pop("certs", {}) - self.method = kwargs.pop( - "method", crypto_util._DEFAULT_SSL_METHOD) + 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) + super().__init__(*args, **kwargs) def _wrap_sock(self) -> None: - self.socket = crypto_util.SSLSocket( + self.socket = cast(socket.socket, crypto_util.SSLSocket( self.socket, cert_selection=self._cert_selection, alpn_selection=getattr(self, '_alpn_selection', None), - method=self.method) + method=self.method)) def _cert_selection(self, connection: SSL.Connection ) -> Tuple[crypto.PKey, crypto.X509]: # pragma: no cover @@ -190,7 +190,7 @@ self.address_family = socket.AF_INET6 else: self.address_family = socket.AF_INET - BaseHTTPServer.HTTPServer.__init__(self, *args, **kwargs) + super().__init__(*args, **kwargs) class HTTP01Server(HTTPServer, ACMEServerMixin): @@ -198,8 +198,8 @@ 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( + super().__init__( + server_address, HTTP01RequestHandler.partial_init( simple_http_resources=resources, timeout=timeout), ipv6=ipv6) @@ -208,7 +208,7 @@ affect the other.""" def __init__(self, *args: Any, **kwargs: Any) -> None: - BaseDualNetworkedServers.__init__(self, HTTP01Server, *args, **kwargs) + super().__init__(HTTP01Server, *args, **kwargs) class HTTP01RequestHandler(BaseHTTPServer.BaseHTTPRequestHandler): @@ -226,7 +226,7 @@ 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) + super().__init__(*args, **kwargs) self.server: HTTP01Server # In parent class BaseHTTPRequestHandler, 'timeout' is a class-level property but we diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/acme-1.22.0/acme.egg-info/PKG-INFO new/acme-1.26.0/acme.egg-info/PKG-INFO --- old/acme-1.22.0/acme.egg-info/PKG-INFO 2021-12-07 23:02:52.000000000 +0100 +++ new/acme-1.26.0/acme.egg-info/PKG-INFO 2022-04-05 19:41:32.000000000 +0200 @@ -1,6 +1,6 @@ Metadata-Version: 2.1 Name: acme -Version: 1.22.0 +Version: 1.26.0 Summary: ACME protocol implementation in Python Home-page: https://github.com/letsencrypt/letsencrypt Author: Certbot Project @@ -12,14 +12,13 @@ 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: Programming Language :: Python :: 3.10 Classifier: Topic :: Internet :: WWW/HTTP Classifier: Topic :: Security -Requires-Python: >=3.6 +Requires-Python: >=3.7 Provides-Extra: docs Provides-Extra: test License-File: LICENSE.txt diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/acme-1.22.0/acme.egg-info/SOURCES.txt new/acme-1.26.0/acme.egg-info/SOURCES.txt --- old/acme-1.22.0/acme.egg-info/SOURCES.txt 2021-12-07 23:02:52.000000000 +0100 +++ new/acme-1.26.0/acme.egg-info/SOURCES.txt 2022-04-05 19:41:33.000000000 +0200 @@ -2,7 +2,6 @@ MANIFEST.in README.rst pytest.ini -setup.cfg setup.py acme/__init__.py acme/challenges.py @@ -14,6 +13,7 @@ acme/magic_typing.py acme/messages.py acme/mixins.py +acme/py.typed acme/standalone.py acme/util.py acme.egg-info/PKG-INFO @@ -72,6 +72,7 @@ tests/testdata/csr.der tests/testdata/csr.pem tests/testdata/dsa512_key.pem +tests/testdata/ec_secp384r1_key.pem tests/testdata/rsa1024_cert.pem tests/testdata/rsa1024_key.pem tests/testdata/rsa2048_cert.pem diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/acme-1.22.0/acme.egg-info/requires.txt new/acme-1.26.0/acme.egg-info/requires.txt --- old/acme-1.22.0/acme.egg-info/requires.txt 2021-12-07 23:02:52.000000000 +0100 +++ new/acme-1.26.0/acme.egg-info/requires.txt 2022-04-05 19:41:33.000000000 +0200 @@ -1,11 +1,11 @@ cryptography>=2.5.0 -josepy>=1.9.0 +josepy>=1.13.0 PyOpenSSL>=17.3.0 pyrfc3339 -pytz -requests>=2.14.2 +pytz>=2019.3 +requests>=2.20.0 requests-toolbelt>=0.3.0 -setuptools>=39.0.1 +setuptools>=41.6.0 [docs] Sphinx>=1.0 @@ -14,3 +14,4 @@ [test] pytest pytest-xdist +typing-extensions diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/acme-1.22.0/setup.cfg new/acme-1.26.0/setup.cfg --- old/acme-1.22.0/setup.cfg 2021-12-07 23:02:52.134568200 +0100 +++ new/acme-1.26.0/setup.cfg 2022-04-05 19:41:33.131385000 +0200 @@ -1,6 +1,3 @@ -[bdist_wheel] -universal = 1 - [egg_info] tag_build = tag_date = 0 diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/acme-1.22.0/setup.py new/acme-1.26.0/setup.py --- old/acme-1.22.0/setup.py 2021-12-07 23:02:46.000000000 +0100 +++ new/acme-1.26.0/setup.py 2022-04-05 19:41:27.000000000 +0200 @@ -3,17 +3,17 @@ from setuptools import find_packages from setuptools import setup -version = '1.22.0' +version = '1.26.0' install_requires = [ 'cryptography>=2.5.0', - 'josepy>=1.9.0', + 'josepy>=1.13.0', 'PyOpenSSL>=17.3.0', 'pyrfc3339', - 'pytz', - 'requests>=2.14.2', + 'pytz>=2019.3', + 'requests>=2.20.0', 'requests-toolbelt>=0.3.0', - 'setuptools>=39.0.1', + 'setuptools>=41.6.0', ] docs_extras = [ @@ -24,6 +24,7 @@ test_extras = [ 'pytest', 'pytest-xdist', + 'typing-extensions', ] setup( @@ -34,14 +35,13 @@ author="Certbot Project", author_email='certbot-...@eff.org', license='Apache License 2.0', - python_requires='>=3.6', + python_requires='>=3.7', 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', diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/acme-1.22.0/tests/challenges_test.py new/acme-1.26.0/tests/challenges_test.py --- old/acme-1.22.0/tests/challenges_test.py 2021-12-07 23:02:45.000000000 +0100 +++ new/acme-1.26.0/tests/challenges_test.py 2022-04-05 19:41:26.000000000 +0200 @@ -6,6 +6,7 @@ import josepy as jose import OpenSSL import requests +from josepy.jwk import JWKEC from acme import errors @@ -401,8 +402,11 @@ 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())) + ec_key_secp384r1 = JWKEC(key=test_util.load_ecdsa_private_key('ec_secp384r1_key.pem')) + for key, alg in [(KEY, jose.RS256), (ec_key_secp384r1, jose.ES384)]: + with self.subTest(key=key, alg=alg): + self.assertTrue(self.msg.check_validation( + self.msg.gen_validation(key, alg=alg), key.public_key())) def test_gen_check_validation_wrong_key(self): key2 = jose.JWKRSA.load(test_util.load_vector('rsa1024_key.pem')) @@ -423,8 +427,7 @@ 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())) + 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: @@ -435,8 +438,14 @@ 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')) + self.assertEqual('_acme-challenge.le.wtf', self.msg.validation_domain_name('le.wtf')) + + def test_validation_domain_name_ecdsa(self): + ec_key_secp384r1 = JWKEC(key=test_util.load_ecdsa_private_key('ec_secp384r1_key.pem')) + self.assertIs(self.msg.check_validation( + self.msg.gen_validation(ec_key_secp384r1, alg=jose.ES384), + ec_key_secp384r1.public_key()), True + ) class DNSResponseTest(unittest.TestCase): @@ -474,8 +483,7 @@ hash(DNSResponse.from_json(self.jmsg_from)) def test_check_validation(self): - self.assertTrue( - self.msg.check_validation(self.chall, KEY.public_key())) + self.assertTrue(self.msg.check_validation(self.chall, KEY.public_key())) class JWSPayloadRFC8555Compliant(unittest.TestCase): diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/acme-1.22.0/tests/fields_test.py new/acme-1.26.0/tests/fields_test.py --- old/acme-1.22.0/tests/fields_test.py 2021-12-07 23:02:45.000000000 +0100 +++ new/acme-1.26.0/tests/fields_test.py 2022-04-05 19:41:26.000000000 +0200 @@ -10,8 +10,8 @@ """Tests for acme.fields.Fixed.""" def setUp(self): - from acme.fields import Fixed - self.field = Fixed('name', 'x') + from acme.fields import fixed + self.field = fixed('name', 'x') def test_decode(self): self.assertEqual('x', self.field.decode('x')) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/acme-1.22.0/tests/standalone_test.py new/acme-1.26.0/tests/standalone_test.py --- old/acme-1.22.0/tests/standalone_test.py 2021-12-07 23:02:45.000000000 +0100 +++ new/acme-1.26.0/tests/standalone_test.py 2022-04-05 19:41:26.000000000 +0200 @@ -165,7 +165,6 @@ 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): @@ -175,7 +174,7 @@ kwargs["bind_and_activate"] = False else: self.address_family = socket.AF_INET - socketserver.TCPServer.__init__(self, *args, **kwargs) + super().__init__(*args, **kwargs) if ipv6: # NB: On Windows, socket.IPPROTO_IPV6 constant may be missing. # We use the corresponding value (41) instead. @@ -202,7 +201,6 @@ self.assertEqual(em.exception.errno, EADDRINUSE) - def test_ports_equal(self): from acme.standalone import BaseDualNetworkedServers servers = BaseDualNetworkedServers( diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/acme-1.22.0/tests/test_util.py new/acme-1.26.0/tests/test_util.py --- old/acme-1.22.0/tests/test_util.py 2021-12-07 23:02:45.000000000 +0100 +++ new/acme-1.26.0/tests/test_util.py 2022-04-05 19:41:26.000000000 +0200 @@ -10,6 +10,7 @@ import josepy as jose from OpenSSL import crypto import pkg_resources +from josepy.util import ComparableECKey def load_vector(*names): @@ -60,6 +61,14 @@ load_vector(*names), password=None, backend=default_backend())) +def load_ecdsa_private_key(*names): + """Load ECDSA private key.""" + loader = _guess_loader(names[-1], serialization.load_pem_private_key, + serialization.load_der_private_key) + return ComparableECKey(loader( + load_vector(*names), password=None, backend=default_backend())) + + def load_pyopenssl_private_key(*names): """Load pyOpenSSL private key.""" loader = _guess_loader( diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/acme-1.22.0/tests/testdata/README new/acme-1.26.0/tests/testdata/README --- old/acme-1.22.0/tests/testdata/README 2021-12-07 23:02:45.000000000 +0100 +++ new/acme-1.26.0/tests/testdata/README 2022-04-05 19:41:26.000000000 +0200 @@ -15,3 +15,7 @@ openssl req -key rsa2048_key.pem -new -subj '/CN=example.com' -x509 -outform DER > cert.der openssl req -key rsa2048_key.pem -new -subj '/CN=example.com' -x509 > rsa2048_cert.pem openssl req -key rsa1024_key.pem -new -subj '/CN=example.com' -x509 > rsa1024_cert.pem + +and for the elliptic key curves: + + openssl genpkey -algorithm EC -out ec_secp384r1.pem -pkeyopt ec_paramgen_curve:P-384 -pkeyopt ec_param_enc:named_curve diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/acme-1.22.0/tests/testdata/ec_secp384r1_key.pem new/acme-1.26.0/tests/testdata/ec_secp384r1_key.pem --- old/acme-1.22.0/tests/testdata/ec_secp384r1_key.pem 1970-01-01 01:00:00.000000000 +0100 +++ new/acme-1.26.0/tests/testdata/ec_secp384r1_key.pem 2022-04-05 19:41:26.000000000 +0200 @@ -0,0 +1,6 @@ +-----BEGIN PRIVATE KEY----- +MIG2AgEAMBAGByqGSM49AgEGBSuBBAAiBIGeMIGbAgEBBDArTn0pbFk3xHfKeXte +xJgS4JVdJQ8mqvezhaNpULZPnwb+mcKLlrj6f5SRM52yREGhZANiAAQcrMoPMVqV +rHnDGGz5HUKLNmXfChlNgsrwsruawXF+M283CA6eckAjTXNyiC/ounWmvtoKsZG0 +2UQOfQUNSCANId/r986yRGc03W6RJSkcRp86qBYjNsLgbZpber/3+M4= +-----END PRIVATE KEY-----