Hello community, here is the log from the commit of package python-acme for openSUSE:Factory checked in at 2019-03-19 09:59:25 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Comparing /work/SRC/openSUSE:Factory/python-acme (Old) and /work/SRC/openSUSE:Factory/.python-acme.new.28833 (New) ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "python-acme" Tue Mar 19 09:59:25 2019 rev:28 rq:685977 version:0.32.0 Changes: -------- --- /work/SRC/openSUSE:Factory/python-acme/python-acme.changes 2019-02-11 21:26:17.327051369 +0100 +++ /work/SRC/openSUSE:Factory/.python-acme.new.28833/python-acme.changes 2019-03-19 09:59:26.960077495 +0100 @@ -1,0 +2,18 @@ +Mon Mar 18 08:17:42 UTC 2019 - Marketa Calabkova <mcalabk...@suse.com> + +- update to 0.32.0 + * Certbot and its acme module now depend on josepy>=1.1.0. + * An ACME CA server may return a "Retry-After" HTTP header on + authorization polling, as specified in the ACME protocol, to + indicate when the next polling should occur. Certbot now reads + this header if set and respect its value. + * The acme module avoids sending the keyAuthorization field in + the JWS payload when responding to a challenge as the field is + not included in the current ACME protocol. To ease the migration + path for ACME CA servers, Certbot and its acme module will first + try the request without the keyAuthorization field but will + temporarily retry the request with the field included if a + malformed error is received. This fallback will be removed in + version 0.34.0. + +------------------------------------------------------------------- Old: ---- acme-0.31.0.tar.gz acme-0.31.0.tar.gz.asc New: ---- acme-0.32.0.tar.gz acme-0.32.0.tar.gz.asc ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Other differences: ------------------ ++++++ python-acme.spec ++++++ --- /var/tmp/diff_new_pack.TvNlHy/_old 2019-03-19 09:59:28.508076873 +0100 +++ /var/tmp/diff_new_pack.TvNlHy/_new 2019-03-19 09:59:28.536076862 +0100 @@ -19,7 +19,7 @@ %{?!python_module:%define python_module() python-%{**} python3-%{**}} %define libname acme Name: python-%{libname} -Version: 0.31.0 +Version: 0.32.0 Release: 0 Summary: Python library for the ACME protocol License: Apache-2.0 @@ -28,28 +28,27 @@ Source0: https://files.pythonhosted.org/packages/source/a/%{libname}/%{libname}-%{version}.tar.gz Source1: https://files.pythonhosted.org/packages/source/a/%{libname}/%{libname}-%{version}.tar.gz.asc Source2: %{name}.keyring -BuildRequires: %{python_module cryptography >= 0.8} -BuildRequires: %{python_module dnspython >= 1.12} -BuildRequires: %{python_module josepy >= 1.0.0} +BuildRequires: %{python_module cryptography >= 1.2.3} +BuildRequires: %{python_module josepy >= 1.1.0} BuildRequires: %{python_module mock} -BuildRequires: %{python_module pyOpenSSL >= 0.13} +BuildRequires: %{python_module pyOpenSSL >= 0.13.1} BuildRequires: %{python_module pyRFC3339} BuildRequires: %{python_module pytest} BuildRequires: %{python_module pytz} -BuildRequires: %{python_module requests >= 2.4.1} +BuildRequires: %{python_module requests >= 2.6.0} BuildRequires: %{python_module requests-toolbelt >= 0.3.0} BuildRequires: %{python_module setuptools} BuildRequires: %{python_module six >= 1.9.0} BuildRequires: %{python_module tox} BuildRequires: fdupes BuildRequires: python-rpm-macros -Requires: python-cryptography >= 0.8 -Requires: python-josepy >= 1.0.0 +Requires: python-cryptography >= 1.2.3 +Requires: python-josepy >= 1.1.0 Requires: python-ndg-httpsclient -Requires: python-pyOpenSSL >= 0.13 +Requires: python-pyOpenSSL >= 0.13.1 Requires: python-pyRFC3339 Requires: python-pytz -Requires: python-requests >= 2.4.1 +Requires: python-requests >= 2.6.0 Requires: python-requests-toolbelt >= 0.3.0 Requires: python-six >= 1.9.0 BuildArch: noarch ++++++ acme-0.31.0.tar.gz -> acme-0.32.0.tar.gz ++++++ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/acme-0.31.0/PKG-INFO new/acme-0.32.0/PKG-INFO --- old/acme-0.31.0/PKG-INFO 2019-02-07 22:20:40.000000000 +0100 +++ new/acme-0.32.0/PKG-INFO 2019-03-06 21:18:19.000000000 +0100 @@ -1,6 +1,6 @@ Metadata-Version: 2.1 Name: acme -Version: 0.31.0 +Version: 0.32.0 Summary: ACME protocol implementation in Python Home-page: https://github.com/letsencrypt/letsencrypt Author: Certbot Project diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/acme-0.31.0/acme/challenges.py new/acme-0.32.0/acme/challenges.py --- old/acme-0.31.0/acme/challenges.py 2019-02-07 22:20:29.000000000 +0100 +++ new/acme-0.32.0/acme/challenges.py 2019-03-06 21:18:08.000000000 +0100 @@ -108,6 +108,10 @@ key_authorization = jose.Field("keyAuthorization") thumbprint_hash_function = hashes.SHA256 + def __init__(self, *args, **kwargs): + super(KeyAuthorizationChallengeResponse, self).__init__(*args, **kwargs) + self._dump_authorization_key(False) + def verify(self, chall, account_public_key): """Verify the key authorization. @@ -140,6 +144,22 @@ return True + def _dump_authorization_key(self, dump): + # type: (bool) -> None + """ + Set if keyAuthorization is dumped in the JSON representation of this ChallengeResponse. + NB: This method is declared as private because it will eventually be removed. + :param bool dump: True to dump the keyAuthorization, False otherwise + """ + object.__setattr__(self, '_dump_auth_key', dump) + + def to_partial_json(self): + jobj = super(KeyAuthorizationChallengeResponse, self).to_partial_json() + if not self._dump_auth_key: # pylint: disable=no-member + jobj.pop('keyAuthorization', None) + + return jobj + @six.add_metaclass(abc.ABCMeta) class KeyAuthorizationChallenge(_TokenChallenge): diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/acme-0.31.0/acme/challenges_test.py new/acme-0.32.0/acme/challenges_test.py --- old/acme-0.31.0/acme/challenges_test.py 2019-02-07 22:20:29.000000000 +0100 +++ new/acme-0.32.0/acme/challenges_test.py 2019-03-06 21:18:08.000000000 +0100 @@ -94,6 +94,9 @@ 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()) + self.msg._dump_authorization_key(True) # pylint: disable=protected-access self.assertEqual(self.jmsg, self.msg.to_partial_json()) def test_from_json(self): @@ -165,6 +168,9 @@ 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()) + self.msg._dump_authorization_key(True) # pylint: disable=protected-access self.assertEqual(self.jmsg, self.msg.to_partial_json()) def test_from_json(self): @@ -285,6 +291,9 @@ self.assertEqual(self.z_domain, self.response.z_domain) 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()) + self.response._dump_authorization_key(True) # pylint: disable=protected-access self.assertEqual(self.jmsg, self.response.to_partial_json()) def test_from_json(self): @@ -419,6 +428,9 @@ 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()) + self.msg._dump_authorization_key(True) # pylint: disable=protected-access self.assertEqual(self.jmsg, self.msg.to_partial_json()) def test_from_json(self): diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/acme-0.31.0/acme/client.py new/acme-0.32.0/acme/client.py --- old/acme-0.31.0/acme/client.py 2019-02-07 22:20:29.000000000 +0100 +++ new/acme-0.32.0/acme/client.py 2019-03-06 21:18:08.000000000 +0100 @@ -17,6 +17,7 @@ from requests.adapters import HTTPAdapter import sys +from acme import challenges from acme import crypto_util from acme import errors from acme import jws @@ -155,7 +156,23 @@ :raises .UnexpectedUpdate: """ - response = self._post(challb.uri, response) + # Because sending keyAuthorization in a response challenge has been removed from the ACME + # spec, it is not included in the KeyAuthorizationResponseChallenge JSON by default. + # However as a migration path, we temporarily expect a malformed error from the server, + # and fallback by resending the challenge response with the keyAuthorization field. + # TODO: Remove this fallback for Certbot 0.34.0 + try: + response = self._post(challb.uri, response) + except messages.Error as error: + if (error.code == 'malformed' + and isinstance(response, challenges.KeyAuthorizationChallengeResponse)): + logger.debug('Error while responding to a challenge without keyAuthorization ' + 'in the JWS, your ACME CA server may not support it:\n%s', error) + logger.debug('Retrying request with keyAuthorization set.') + response._dump_authorization_key(True) # pylint: disable=protected-access + response = self._post(challb.uri, response) + else: + raise try: authzr_uri = response.links['up']['url'] except KeyError: @@ -739,8 +756,7 @@ 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, - content_type=DER_CONTENT_TYPE).text + certificate_response = self._post_as_get(body.certificate).text return orderr.update(body=body, fullchain_pem=certificate_response) raise errors.TimeoutError() @@ -782,7 +798,7 @@ except messages.Error as error: if error.code == 'malformed': logger.debug('Error during a POST-as-GET request, ' - 'your ACME CA may not support it:\n%s', error) + 'your ACME CA server may not support it:\n%s', error) logger.debug('Retrying request with GET.') else: # pragma: no cover raise @@ -1192,10 +1208,7 @@ def _post_once(self, url, obj, content_type=JOSE_CONTENT_TYPE, acme_version=1, **kwargs): - try: - new_nonce_url = kwargs.pop('new_nonce_url') - except KeyError: - new_nonce_url = None + 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) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/acme-0.31.0/acme/client_test.py new/acme-0.32.0/acme/client_test.py --- old/acme-0.31.0/acme/client_test.py 2019-02-07 22:20:29.000000000 +0100 +++ new/acme-0.32.0/acme/client_test.py 2019-03-06 21:18:08.000000000 +0100 @@ -463,6 +463,34 @@ errors.ClientError, self.client.answer_challenge, self.challr.body, challenges.DNSResponse(validation=None)) + def test_answer_challenge_key_authorization_fallback(self): + self.response.links['up'] = {'url': self.challr.authzr_uri} + self.response.json.return_value = self.challr.body.to_json() + + def _wrapper_post(url, obj, *args, **kwargs): # pylint: disable=unused-argument + """ + Simulate an old ACME CA server, that would respond a 'malformed' + error if keyAuthorization is missing. + """ + jobj = obj.to_partial_json() + if 'keyAuthorization' not in jobj: + raise messages.Error.with_code('malformed') + return self.response + self.net.post.side_effect = _wrapper_post + + # This challenge response is of type KeyAuthorizationChallengeResponse, so the fallback + # should be triggered, and avoid an exception. + http_chall_response = challenges.HTTP01Response(key_authorization='test', + resource=mock.MagicMock()) + self.client.answer_challenge(self.challr.body, http_chall_response) + + # This challenge response is not of type KeyAuthorizationChallengeResponse, so the fallback + # should not be triggered, leading to an exception. + dns_chall_response = challenges.DNSResponse(validation=None) + self.assertRaises( + errors.Error, self.client.answer_challenge, + self.challr.body, dns_chall_response) + def test_retry_after_date(self): self.response.headers['Retry-After'] = 'Fri, 31 Dec 1999 23:59:59 GMT' self.assertEqual( diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/acme-0.31.0/acme.egg-info/PKG-INFO new/acme-0.32.0/acme.egg-info/PKG-INFO --- old/acme-0.31.0/acme.egg-info/PKG-INFO 2019-02-07 22:20:40.000000000 +0100 +++ new/acme-0.32.0/acme.egg-info/PKG-INFO 2019-03-06 21:18:19.000000000 +0100 @@ -1,6 +1,6 @@ Metadata-Version: 2.1 Name: acme -Version: 0.31.0 +Version: 0.32.0 Summary: ACME protocol implementation in Python Home-page: https://github.com/letsencrypt/letsencrypt Author: Certbot Project diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/acme-0.31.0/acme.egg-info/SOURCES.txt new/acme-0.32.0/acme.egg-info/SOURCES.txt --- old/acme-0.31.0/acme.egg-info/SOURCES.txt 2019-02-07 22:20:40.000000000 +0100 +++ new/acme-0.32.0/acme.egg-info/SOURCES.txt 2019-03-06 21:18:19.000000000 +0100 @@ -70,6 +70,7 @@ docs/api/messages.rst docs/api/standalone.rst docs/man/jws.rst +examples/http01_example.py examples/standalone/README examples/standalone/localhost/cert.pem examples/standalone/localhost/key.pem \ No newline at end of file diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/acme-0.31.0/acme.egg-info/requires.txt new/acme-0.32.0/acme.egg-info/requires.txt --- old/acme-0.31.0/acme.egg-info/requires.txt 2019-02-07 22:20:40.000000000 +0100 +++ new/acme-0.32.0/acme.egg-info/requires.txt 2019-03-06 21:18:19.000000000 +0100 @@ -1,5 +1,5 @@ cryptography>=1.2.3 -josepy>=1.0.0 +josepy>=1.1.0 mock PyOpenSSL>=0.13.1 pyrfc3339 diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/acme-0.31.0/examples/http01_example.py new/acme-0.32.0/examples/http01_example.py --- old/acme-0.31.0/examples/http01_example.py 1970-01-01 01:00:00.000000000 +0100 +++ new/acme-0.32.0/examples/http01_example.py 2019-03-06 21:18:08.000000000 +0100 @@ -0,0 +1,240 @@ +"""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 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 +import josepy as jose + +# 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 = ('f...@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 = 'newf...@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() diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/acme-0.31.0/setup.py new/acme-0.32.0/setup.py --- old/acme-0.31.0/setup.py 2019-02-07 22:20:31.000000000 +0100 +++ new/acme-0.32.0/setup.py 2019-03-06 21:18:09.000000000 +0100 @@ -3,7 +3,7 @@ from setuptools.command.test import test as TestCommand import sys -version = '0.31.0' +version = '0.32.0' # Please update tox.ini when modifying dependency version requirements install_requires = [ @@ -11,7 +11,9 @@ # rsa_recover_prime_factors (>=0.8) 'cryptography>=1.2.3', # formerly known as acme.jose: - 'josepy>=1.0.0', + # 1.1.0+ is required to avoid the warnings described at + # https://github.com/certbot/josepy/issues/13. + 'josepy>=1.1.0', # Connection.set_tlsext_host_name (>=0.13) 'mock', 'PyOpenSSL>=0.13.1', @@ -34,6 +36,7 @@ 'sphinx_rtd_theme', ] + class PyTest(TestCommand): user_options = [] @@ -48,6 +51,7 @@ errno = pytest.main(shlex.split(self.pytest_args)) sys.exit(errno) + setup( name='acme', version=version, @@ -80,7 +84,7 @@ 'dev': dev_extras, 'docs': docs_extras, }, - tests_require=["pytest"], test_suite='acme', + tests_require=["pytest"], cmdclass={"test": PyTest}, )