Hello community, here is the log from the commit of package python-acme for openSUSE:Factory checked in at 2017-05-17 17:18:46 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Comparing /work/SRC/openSUSE:Factory/python-acme (Old) and /work/SRC/openSUSE:Factory/.python-acme.new (New) ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "python-acme" Wed May 17 17:18:46 2017 rev:5 rq:495251 version:0.14.0 Changes: -------- --- /work/SRC/openSUSE:Factory/python-acme/python-acme.changes 2017-04-06 11:01:48.770444504 +0200 +++ /work/SRC/openSUSE:Factory/.python-acme.new/python-acme.changes 2017-05-17 17:19:30.751273964 +0200 @@ -1,0 +2,17 @@ +Tue May 16 10:50:51 UTC 2017 - ec...@opensuse.org + +- fix build error in Tumbleweed + +------------------------------------------------------------------- +Mon May 15 10:51:46 UTC 2017 - ec...@opensuse.org + +- update to 0.14.0 + - No changelog provides by upstream + +------------------------------------------------------------------- +Tue Apr 25 20:53:13 UTC 2017 - ec...@opensuse.org + +- update to 0.13.0 + - No changelog provides by upstream + +------------------------------------------------------------------- Old: ---- acme-0.12.0.tar.gz acme-0.12.0.tar.gz.asc New: ---- acme-0.14.0.tar.gz acme-0.14.0.tar.gz.asc ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Other differences: ------------------ ++++++ python-acme.spec ++++++ --- /var/tmp/diff_new_pack.TsT36B/_old 2017-05-17 17:19:31.391183720 +0200 +++ /var/tmp/diff_new_pack.TsT36B/_new 2017-05-17 17:19:31.395183156 +0200 @@ -18,7 +18,7 @@ %define libname acme Name: python-%{libname} -Version: 0.12.0 +Version: 0.14.0 Release: 0 Summary: Python library for the ACME protocol License: Apache-2.0 @@ -28,16 +28,22 @@ Source1: https://files.pythonhosted.org/packages/source/a/%{libname}/%{libname}-%{version}.tar.gz.asc Source2: %{name}.keyring BuildRequires: fdupes +BuildRequires: python-argparse BuildRequires: python-cryptography >= 0.8 BuildRequires: python-devel BuildRequires: python-dnspython >= 1.12 BuildRequires: python-mock BuildRequires: python-ndg-httpsclient BuildRequires: python-nose +BuildRequires: python-packaging BuildRequires: python-pyOpenSSL >= 0.13 BuildRequires: python-pyRFC3339 BuildRequires: python-pytz +%if 0%{?suse_version} >= 1330 +BuildRequires: python2-requests >= 2.10 +%else BuildRequires: python-requests >= 2.10 +%endif BuildRequires: python-setuptools >= 11.3 BuildRequires: python-six >= 1.5.2 BuildRequires: python-sphinx @@ -45,7 +51,7 @@ BuildRequires: python-sphinxcontrib-programoutput BuildRequires: python-tox BuildRequires: python-werkzeug -BuildRequires: python-packaging +Requires: python-argparse Requires: python-cryptography >= 0.8 Requires: python-dnspython >= 1.12 Requires: python-mock @@ -54,7 +60,11 @@ Requires: python-pyRFC3339 Requires: python-pyasn1 Requires: python-pytz +%if 0%{?suse_version} >= 1330 +Requires: python2-requests >= 2.10 +%else Requires: python-requests >= 2.10 +%endif Requires: python-six >= 1.5.2 Requires: python-werkzeug BuildArch: noarch ++++++ acme-0.12.0.tar.gz -> acme-0.14.0.tar.gz ++++++ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/acme-0.12.0/PKG-INFO new/acme-0.14.0/PKG-INFO --- old/acme-0.12.0/PKG-INFO 2017-03-02 23:01:44.000000000 +0100 +++ new/acme-0.14.0/PKG-INFO 2017-05-05 01:45:31.000000000 +0200 @@ -1,6 +1,6 @@ Metadata-Version: 1.1 Name: acme -Version: 0.12.0 +Version: 0.14.0 Summary: ACME protocol implementation in Python Home-page: https://github.com/letsencrypt/letsencrypt Author: Certbot Project @@ -19,5 +19,6 @@ Classifier: Programming Language :: Python :: 3.3 Classifier: Programming Language :: Python :: 3.4 Classifier: Programming Language :: Python :: 3.5 +Classifier: Programming Language :: Python :: 3.6 Classifier: Topic :: Internet :: WWW/HTTP Classifier: Topic :: Security diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/acme-0.12.0/acme/challenges.py new/acme-0.14.0/acme/challenges.py --- old/acme-0.12.0/acme/challenges.py 2017-03-02 23:01:28.000000000 +0100 +++ new/acme-0.14.0/acme/challenges.py 2017-05-05 01:45:16.000000000 +0200 @@ -5,7 +5,7 @@ import logging import socket -from cryptography.hazmat.primitives import hashes +from cryptography.hazmat.primitives import hashes # type: ignore import OpenSSL import requests @@ -23,7 +23,7 @@ class Challenge(jose.TypedJSONObjectWithFields): # _fields_to_partial_json | pylint: disable=abstract-method """ACME challenge.""" - TYPES = {} + TYPES = {} # type: dict @classmethod def from_json(cls, jobj): @@ -37,7 +37,7 @@ class ChallengeResponse(jose.TypedJSONObjectWithFields): # _fields_to_partial_json | pylint: disable=abstract-method """ACME challenge response.""" - TYPES = {} + TYPES = {} # type: dict resource_type = 'challenge' resource = fields.Resource(resource_type) @@ -445,7 +445,7 @@ """ # pylint: disable=protected-access sans = crypto_util._pyopenssl_cert_or_req_san(cert) - logger.debug('Certificate %s. SANs: %s', cert.digest('sha1'), sans) + logger.debug('Certificate %s. SANs: %s', cert.digest('sha256'), sans) return self.z_domain.decode() in sans def simple_verify(self, chall, domain, account_public_key, diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/acme-0.12.0/acme/client.py new/acme-0.14.0/acme/client.py --- old/acme-0.12.0/acme/client.py 2017-03-02 23:01:28.000000000 +0100 +++ new/acme-0.14.0/acme/client.py 2017-05-05 01:45:16.000000000 +0200 @@ -28,11 +28,13 @@ # https://urllib3.readthedocs.org/en/latest/security.html#insecureplatformwarning if sys.version_info < (2, 7, 9): # pragma: no cover try: - requests.packages.urllib3.contrib.pyopenssl.inject_into_urllib3() + requests.packages.urllib3.contrib.pyopenssl.inject_into_urllib3() # type: ignore except AttributeError: import urllib3.contrib.pyopenssl # pylint: disable=import-error urllib3.contrib.pyopenssl.inject_into_urllib3() +DEFAULT_NETWORK_TIMEOUT = 45 + DER_CONTENT_TYPE = 'application/pkix-cert' @@ -71,20 +73,13 @@ self.directory = directory @classmethod - def _regr_from_response(cls, response, uri=None, new_authzr_uri=None, - terms_of_service=None): + def _regr_from_response(cls, response, uri=None, terms_of_service=None): if 'terms-of-service' in response.links: terms_of_service = response.links['terms-of-service']['url'] - if 'next' in response.links: - new_authzr_uri = response.links['next']['url'] - - if new_authzr_uri is None: - raise errors.ClientError('"next" link missing') return messages.RegistrationResource( body=messages.Registration.from_json(response.json()), uri=response.headers.get('Location', uri), - new_authzr_uri=new_authzr_uri, terms_of_service=terms_of_service) def register(self, new_reg=None): @@ -117,7 +112,7 @@ # (c.f. acme-spec #94) return self._regr_from_response( - response, uri=regr.uri, new_authzr_uri=regr.new_authzr_uri, + response, uri=regr.uri, terms_of_service=regr.terms_of_service) def update_registration(self, regr, update=None): @@ -134,8 +129,6 @@ update = regr.body if update is None else update body = messages.UpdateRegistration(**dict(update)) updated_regr = self._send_recv_regr(regr, body=body) - if updated_regr != regr: - raise errors.UnexpectedUpdate(regr) return updated_regr def deactivate_registration(self, regr): @@ -174,19 +167,10 @@ return self.update_registration( regr.update(body=regr.body.update(agreement=regr.terms_of_service))) - def _authzr_from_response(self, response, identifier, - uri=None, new_cert_uri=None): - # pylint: disable=no-self-use - if new_cert_uri is None: - try: - new_cert_uri = response.links['next']['url'] - except KeyError: - raise errors.ClientError('"next" link missing') - + def _authzr_from_response(self, response, identifier, uri=None): authzr = messages.AuthorizationResource( body=messages.Authorization.from_json(response.json()), - uri=response.headers.get('Location', uri), - new_cert_uri=new_cert_uri) + uri=response.headers.get('Location', uri)) if authzr.body.identifier != identifier: raise errors.UnexpectedUpdate(authzr) return authzr @@ -195,17 +179,16 @@ """Request challenges. :param .messages.Identifier identifier: Identifier to be challenged. - :param str new_authzr_uri: ``new-authorization`` URI. If omitted, - will default to value found in ``directory``. + :param str new_authzr_uri: Deprecated. Do not use. :returns: Authorization Resource. :rtype: `.AuthorizationResource` """ + if new_authzr_uri is not None: + logger.debug("request_challenges with new_authzr_uri deprecated.") new_authz = messages.NewAuthorization(identifier=identifier) - response = self.net.post(self.directory.new_authz - if new_authzr_uri is None else new_authzr_uri, - new_authz) + response = self.net.post(self.directory.new_authz, new_authz) # TODO: handle errors assert response.status_code == http_client.CREATED return self._authzr_from_response(response, identifier) @@ -219,6 +202,7 @@ documentation. :param str domain: Domain name to be challenged. + :param str new_authzr_uri: Deprecated. Do not use. :returns: Authorization Resource. :rtype: `.AuthorizationResource` @@ -300,8 +284,7 @@ """ response = self.net.get(authzr.uri) updated_authzr = self._authzr_from_response( - response, authzr.body.identifier, authzr.uri, authzr.new_cert_uri) - # TODO: check and raise UnexpectedUpdate + response, authzr.body.identifier, authzr.uri) return updated_authzr, response def request_issuance(self, csr, authzrs): @@ -324,7 +307,7 @@ content_type = DER_CONTENT_TYPE # TODO: add 'cert_type 'argument response = self.net.post( - authzrs[0].new_cert_uri, # TODO: acme-spec #90 + self.directory.new_cert, req, content_type=content_type, headers={'Accept': content_type}) @@ -377,14 +360,18 @@ # priority queue with datetime.datetime (based on Retry-After) as key, # and original Authorization Resource as value - waiting = [(datetime.datetime.now(), authzr) for authzr in authzrs] + 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 = dict((authzr, authzr) for authzr in authzrs) while waiting: # find the smallest Retry-After, and sleep if necessary - when, authzr = heapq.heappop(waiting) + when, index, authzr = heapq.heappop(waiting) now = datetime.datetime.now() if when > now: seconds = (when - now).seconds @@ -403,7 +390,7 @@ if attempts[authzr] < max_attempts: # push back to the priority queue, with updated retry_after heapq.heappush(waiting, (self.retry_after( - response, default=mintime), authzr)) + response, default=mintime), index, authzr)) else: exhausted.add(authzr) @@ -522,13 +509,14 @@ REPLAY_NONCE_HEADER = 'Replay-Nonce' def __init__(self, key, alg=jose.RS256, verify_ssl=True, - user_agent='acme-python'): + user_agent='acme-python', timeout=DEFAULT_NETWORK_TIMEOUT): self.key = key self.alg = alg self.verify_ssl = verify_ssl self._nonces = set() self.user_agent = user_agent self.session = requests.Session() + self._default_timeout = timeout def __del__(self): self.session.close() @@ -627,7 +615,7 @@ kwargs['verify'] = self.verify_ssl kwargs.setdefault('headers', {}) kwargs['headers'].setdefault('User-Agent', self.user_agent) - kwargs.setdefault('timeout', 45) # timeout after 45 seconds + kwargs.setdefault('timeout', self._default_timeout) response = self.session.request(method, url, *args, **kwargs) # If content is DER, log the base64 of it instead of raw bytes, to keep # binary data out of the logs. diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/acme-0.12.0/acme/client_test.py new/acme-0.14.0/acme/client_test.py --- old/acme-0.12.0/acme/client_test.py 2017-03-02 23:01:28.000000000 +0100 +++ new/acme-0.14.0/acme/client_test.py 2017-05-05 01:45:16.000000000 +0200 @@ -40,6 +40,8 @@ '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', }) from acme.client import Client @@ -56,7 +58,6 @@ self.new_reg = messages.NewRegistration(**dict(reg)) self.regr = messages.RegistrationResource( body=reg, uri='https://www.letsencrypt-demo.org/acme/reg/1', - new_authzr_uri='https://www.letsencrypt-demo.org/acme/new-reg', terms_of_service='https://www.letsencrypt-demo.org/tos') # Authorization @@ -72,8 +73,7 @@ typ=messages.IDENTIFIER_FQDN, value='example.com'), challenges=(challb,), combinations=None) self.authzr = messages.AuthorizationResource( - body=self.authz, uri=authzr_uri, - new_cert_uri='https://www.letsencrypt-demo.org/acme/new-cert') + body=self.authz, uri=authzr_uri) # Request issuance self.certr = messages.CertificateResource( @@ -98,18 +98,12 @@ self.response.json.return_value = self.regr.body.to_json() self.response.headers['Location'] = self.regr.uri self.response.links.update({ - 'next': {'url': self.regr.new_authzr_uri}, '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_register_missing_next(self): - self.response.status_code = http_client.CREATED - self.assertRaises( - errors.ClientError, self.client.register, self.new_reg) - def test_update_registration(self): # "Instance of 'Field' has no to_json/update member" bug: # pylint: disable=no-member @@ -121,8 +115,6 @@ # TODO: split here and separate test self.response.json.return_value = self.regr.body.update( contact=()).to_json() - self.assertRaises( - errors.UnexpectedUpdate, self.client.update_registration, self.regr) def test_deactivate_account(self): self.response.headers['Location'] = self.regr.uri @@ -130,25 +122,10 @@ self.assertEqual(self.regr, self.client.deactivate_registration(self.regr)) - def test_deactivate_account_bad_registration_returned(self): - self.response.headers['Location'] = self.regr.uri - self.response.json.return_value = "some wrong registration thing" - self.assertRaises( - errors.UnexpectedUpdate, - 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_query_registration_updates_new_authzr_uri(self): - self.response.json.return_value = self.regr.body.to_json() - self.response.links = {'next': {'url': 'UPDATED'}} - self.assertEqual( - 'UPDATED', - self.client.query_registration(self.regr).new_authzr_uri) - def test_agree_to_tos(self): self.client.update_registration = mock.Mock() self.client.agree_to_tos(self.regr) @@ -159,9 +136,6 @@ self.response.status_code = http_client.CREATED self.response.headers['Location'] = self.authzr.uri self.response.json.return_value = self.authz.to_json() - self.response.links = { - 'next': {'url': self.authzr.new_cert_uri}, - } def test_request_challenges(self): self._prepare_response_for_request_challenges() @@ -170,10 +144,18 @@ self.directory.new_authz, messages.NewAuthorization(identifier=self.identifier)) + 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)) + def test_request_challenges_custom_uri(self): self._prepare_response_for_request_challenges() - self.client.request_challenges(self.identifier, 'URI') - self.net.post.assert_called_once_with('URI', mock.ANY) + self.client.request_challenges(self.identifier) + self.net.post.assert_called_once_with( + 'https://www.letsencrypt-demo.org/acme/new-authz', mock.ANY) def test_request_challenges_unexpected_update(self): self._prepare_response_for_request_challenges() @@ -181,12 +163,7 @@ identifier=self.identifier.update(value='foo')).to_json() self.assertRaises( errors.UnexpectedUpdate, self.client.request_challenges, - self.identifier, self.authzr.uri) - - def test_request_challenges_missing_next(self): - self.response.status_code = http_client.CREATED - self.assertRaises(errors.ClientError, self.client.request_challenges, - self.identifier) + self.identifier) def test_request_domain_challenges(self): self.client.request_challenges = mock.MagicMock() @@ -194,12 +171,6 @@ self.client.request_challenges(self.identifier), self.client.request_domain_challenges('example.com')) - def test_request_domain_challenges_custom_uri(self): - self.client.request_challenges = mock.MagicMock() - self.assertEqual( - self.client.request_challenges(self.identifier, 'URI'), - self.client.request_domain_challenges('example.com', 'URI')) - def test_answer_challenge(self): self.response.links['up'] = {'url': self.challr.authzr_uri} self.response.json.return_value = self.challr.body.to_json() diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/acme-0.12.0/acme/crypto_util.py new/acme-0.14.0/acme/crypto_util.py --- old/acme-0.12.0/acme/crypto_util.py 2017-03-02 23:01:28.000000000 +0100 +++ new/acme-0.14.0/acme/crypto_util.py 2017-05-05 01:45:16.000000000 +0200 @@ -23,7 +23,7 @@ # 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_TLSSNI01_SSL_METHOD = OpenSSL.SSL.SSLv23_METHOD +_DEFAULT_TLSSNI01_SSL_METHOD = OpenSSL.SSL.SSLv23_METHOD # type: ignore class SSLSocket(object): # pylint: disable=too-few-public-methods @@ -149,6 +149,36 @@ raise errors.Error(error) return client_ssl.get_peer_certificate() +def make_csr(private_key_pem, domains, must_staple=False): + """Generate a CSR containing a list of domains 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). + :returns: buffer PEM-encoded Certificate Signing Request. + """ + private_key = OpenSSL.crypto.load_privatekey( + OpenSSL.crypto.FILETYPE_PEM, private_key_pem) + csr = OpenSSL.crypto.X509Req() + extensions = [ + OpenSSL.crypto.X509Extension( + b'subjectAltName', + critical=False, + value=', '.join('DNS:' + d for d in domains).encode('ascii') + ), + ] + if must_staple: + extensions.append(OpenSSL.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 OpenSSL.crypto.dump_certificate_request( + OpenSSL.crypto.FILETYPE_PEM, csr) def _pyopenssl_cert_or_req_san(cert_or_req): """Get Subject Alternative Names from certificate or CSR using pyOpenSSL. diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/acme-0.12.0/acme/crypto_util_test.py new/acme-0.14.0/acme/crypto_util_test.py --- old/acme-0.12.0/acme/crypto_util_test.py 2017-03-02 23:01:28.000000000 +0100 +++ new/acme-0.14.0/acme/crypto_util_test.py 2017-05-05 01:45:16.000000000 +0200 @@ -6,7 +6,7 @@ import unittest import six -from six.moves import socketserver # pylint: disable=import-error +from six.moves import socketserver #type: ignore # pylint: disable=import-error import OpenSSL @@ -151,6 +151,53 @@ self.serial_num.append(cert.get_serial_number()) self.assertTrue(len(set(self.serial_num)) > 1) +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.assertTrue(b'--BEGIN CERTIFICATE REQUEST--' in csr_pem) + self.assertTrue(b'--END CERTIFICATE REQUEST--' in csr_pem) + csr = OpenSSL.crypto.load_certificate_request( + OpenSSL.crypto.FILETYPE_PEM, csr_pem) + # In pyopenssl 0.13 (used with TOXENV=py26-oldest and 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.assertEquals(len(csr.get_extensions()), 1) + self.assertEquals(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_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=py26-oldest and 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.assertEquals(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") if __name__ == '__main__': unittest.main() # pragma: no cover diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/acme-0.12.0/acme/jose/json_util.py new/acme-0.14.0/acme/jose/json_util.py --- old/acme-0.12.0/acme/jose/json_util.py 2017-03-02 23:01:28.000000000 +0100 +++ new/acme-0.14.0/acme/jose/json_util.py 2017-05-05 01:45:16.000000000 +0200 @@ -267,7 +267,7 @@ if missing: raise errors.DeserializationError( - 'The following field are required: {0}'.format( + 'The following fields are required: {0}'.format( ','.join(missing))) @classmethod diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/acme-0.12.0/acme/jose/jwa.py new/acme-0.14.0/acme/jose/jwa.py --- old/acme-0.12.0/acme/jose/jwa.py 2017-03-02 23:01:28.000000000 +0100 +++ new/acme-0.14.0/acme/jose/jwa.py 2017-05-05 01:45:16.000000000 +0200 @@ -9,9 +9,9 @@ import cryptography.exceptions from cryptography.hazmat.backends import default_backend -from cryptography.hazmat.primitives import hashes -from cryptography.hazmat.primitives import hmac -from cryptography.hazmat.primitives.asymmetric import padding +from cryptography.hazmat.primitives import hashes # type: ignore +from cryptography.hazmat.primitives import hmac # type: ignore +from cryptography.hazmat.primitives.asymmetric import padding # type: ignore from acme.jose import errors from acme.jose import interfaces @@ -28,9 +28,9 @@ """JSON Web Algorithm.""" -class JWASignature(JWA, collections.Hashable): +class JWASignature(JWA, collections.Hashable): # type: ignore """JSON Web Signature Algorithm.""" - SIGNATURES = {} + SIGNATURES = {} # type: dict def __init__(self, name): self.name = name diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/acme-0.12.0/acme/jose/jwk.py new/acme-0.14.0/acme/jose/jwk.py --- old/acme-0.12.0/acme/jose/jwk.py 2017-03-02 23:01:28.000000000 +0100 +++ new/acme-0.14.0/acme/jose/jwk.py 2017-05-05 01:45:16.000000000 +0200 @@ -6,9 +6,9 @@ import cryptography.exceptions from cryptography.hazmat.backends import default_backend -from cryptography.hazmat.primitives import hashes +from cryptography.hazmat.primitives import hashes # type: ignore from cryptography.hazmat.primitives import serialization -from cryptography.hazmat.primitives.asymmetric import ec +from cryptography.hazmat.primitives.asymmetric import ec # type: ignore from cryptography.hazmat.primitives.asymmetric import rsa import six @@ -25,8 +25,8 @@ # pylint: disable=too-few-public-methods """JSON Web Key.""" type_field_name = 'kty' - TYPES = {} - cryptography_key_types = () + TYPES = {} # type: dict + cryptography_key_types = () # type: tuple """Subclasses should override.""" required = NotImplemented diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/acme-0.12.0/acme/jose/jws.py new/acme-0.14.0/acme/jose/jws.py --- old/acme-0.12.0/acme/jose/jws.py 2017-03-02 23:01:28.000000000 +0100 +++ new/acme-0.14.0/acme/jose/jws.py 2017-05-05 01:45:16.000000000 +0200 @@ -121,12 +121,12 @@ # x5c does NOT use JOSE Base64 (4.1.6) - @x5c.encoder + @x5c.encoder # type: ignore def x5c(value): # pylint: disable=missing-docstring,no-self-argument return [base64.b64encode(OpenSSL.crypto.dump_certificate( OpenSSL.crypto.FILETYPE_ASN1, cert.wrapped)) for cert in value] - @x5c.decoder + @x5c.decoder # type: ignore def x5c(value): # pylint: disable=missing-docstring,no-self-argument try: return tuple(util.ComparableX509(OpenSSL.crypto.load_certificate( @@ -157,12 +157,12 @@ 'signature', decoder=json_util.decode_b64jose, encoder=json_util.encode_b64jose) - @protected.encoder + @protected.encoder # type: ignore def protected(value): # pylint: disable=missing-docstring,no-self-argument # wrong type guess (Signature, not bytes) | pylint: disable=no-member return json_util.encode_b64jose(value.encode('utf-8')) - @protected.decoder + @protected.decoder # type: ignore def protected(value): # pylint: disable=missing-docstring,no-self-argument return json_util.decode_b64jose(value).decode('utf-8') diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/acme-0.12.0/acme/jose/util.py new/acme-0.14.0/acme/jose/util.py --- old/acme-0.12.0/acme/jose/util.py 2017-03-02 23:01:28.000000000 +0100 +++ new/acme-0.14.0/acme/jose/util.py 2017-05-05 01:45:16.000000000 +0200 @@ -134,7 +134,7 @@ return hash((self.__class__, pub.n, pub.e)) -class ImmutableMap(collections.Mapping, collections.Hashable): +class ImmutableMap(collections.Mapping, collections.Hashable): # type: ignore # pylint: disable=too-few-public-methods """Immutable key to value mapping with attribute access.""" @@ -180,7 +180,7 @@ for key, value in six.iteritems(self))) -class frozendict(collections.Mapping, collections.Hashable): +class frozendict(collections.Mapping, collections.Hashable): # type: ignore # pylint: disable=invalid-name,too-few-public-methods """Frozen dictionary.""" __slots__ = ('_items', '_keys') diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/acme-0.12.0/acme/jws.py new/acme-0.14.0/acme/jws.py --- old/acme-0.12.0/acme/jws.py 2017-03-02 23:01:28.000000000 +0100 +++ new/acme-0.14.0/acme/jws.py 2017-05-05 01:45:16.000000000 +0200 @@ -1,14 +1,18 @@ -"""ACME JOSE JWS.""" +"""ACME-specific JWS. + +The JWS implementation in acme.jose 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 acme.jose. +""" from acme import jose class Header(jose.Header): - """ACME JOSE Header. - - .. todo:: Implement ``acmePath``. - + """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.decoder def nonce(value): # pylint: disable=missing-docstring,no-self-argument @@ -20,7 +24,7 @@ class Signature(jose.Signature): - """ACME 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 @@ -34,11 +38,17 @@ class JWS(jose.JWS): - """ACME JWS.""" + """ACME-specific JWS. Includes none, url, and kid in protected header.""" signature_cls = Signature __slots__ = jose.JWS._orig_slots # pylint: disable=no-member @classmethod - def sign(cls, payload, key, alg, nonce): # pylint: disable=arguments-differ + # pylint: disable=arguments-differ,too-many-arguments + def sign(cls, payload, key, alg, nonce, url=None, kid=None): + # 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(JWS, cls).sign(payload, key=key, alg=alg, - protect=frozenset(['nonce']), nonce=nonce) + protect=frozenset(['nonce', 'url', 'kid']), + nonce=nonce, url=url, kid=kid, + include_jwk=include_jwk) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/acme-0.12.0/acme/jws_test.py new/acme-0.14.0/acme/jws_test.py --- old/acme-0.12.0/acme/jws_test.py 2017-03-02 23:01:28.000000000 +0100 +++ new/acme-0.14.0/acme/jws_test.py 2017-05-05 01:45:16.000000000 +0200 @@ -37,16 +37,30 @@ self.privkey = KEY self.pubkey = self.privkey.public_key() self.nonce = jose.b64encode(b'Nonce') + self.url = 'hi' + self.kid = 'baaaaa' - def test_it(self): + 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) + 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.assertEqual(jws.signature.combined.jwk, None) # 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.assertEqual(jws.signature.combined.kid, None) + self.assertEqual(jws.signature.combined.jwk, self.pubkey) + if __name__ == '__main__': unittest.main() # pragma: no cover diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/acme-0.12.0/acme/messages.py new/acme-0.14.0/acme/messages.py --- old/acme-0.12.0/acme/messages.py 2017-03-02 23:01:28.000000000 +0100 +++ new/acme-0.14.0/acme/messages.py 2017-05-05 01:45:16.000000000 +0200 @@ -98,7 +98,7 @@ if part is not None) -class _Constant(jose.JSONDeSerializable, collections.Hashable): +class _Constant(jose.JSONDeSerializable, collections.Hashable): # type: ignore """ACME constant.""" __slots__ = ('name',) POSSIBLE_NAMES = NotImplemented @@ -132,7 +132,7 @@ class Status(_Constant): """ACME "status" field.""" - POSSIBLE_NAMES = {} + POSSIBLE_NAMES = {} # type: dict STATUS_UNKNOWN = Status('unknown') STATUS_PENDING = Status('pending') STATUS_PROCESSING = Status('processing') @@ -143,7 +143,7 @@ class IdentifierType(_Constant): """ACME identifier type.""" - POSSIBLE_NAMES = {} + POSSIBLE_NAMES = {} # type: dict IDENTIFIER_FQDN = IdentifierType('dns') # IdentifierDNS in Boulder @@ -161,7 +161,7 @@ class Directory(jose.JSONDeSerializable): """Directory.""" - _REGISTERED_TYPES = {} + _REGISTERED_TYPES = {} # type: dict class Meta(jose.JSONObjectWithFields): """Directory Meta.""" @@ -191,7 +191,7 @@ try: return self[name.replace('_', '-')] except KeyError as error: - raise AttributeError(str(error)) + raise AttributeError(str(error) + ': ' + name) def __getitem__(self, name): try: @@ -237,10 +237,6 @@ :ivar tuple contact: Contact information following ACME spec, `tuple` of `unicode`. :ivar unicode agreement: - :ivar unicode authorizations: URI where - `messages.Registration.Authorizations` can be found. - :ivar unicode certificates: URI where - `messages.Registration.Certificates` can be found. """ # on new-reg key server ignores 'key' and populates it based on @@ -248,26 +244,8 @@ key = jose.Field('key', omitempty=True, decoder=jose.JWK.from_json) contact = jose.Field('contact', omitempty=True, default=()) agreement = jose.Field('agreement', omitempty=True) - authorizations = jose.Field('authorizations', omitempty=True) - certificates = jose.Field('certificates', omitempty=True) status = jose.Field('status', omitempty=True) - class Authorizations(jose.JSONObjectWithFields): - """Authorizations granted to Account in the process of registration. - - :ivar tuple authorizations: URIs to Authorization Resources. - - """ - authorizations = jose.Field('authorizations') - - class Certificates(jose.JSONObjectWithFields): - """Certificates granted to Account in the process of registration. - - :ivar tuple certificates: URIs to Certificate Resources. - - """ - certificates = jose.Field('certificates') - phone_prefix = 'tel:' email_prefix = 'mailto:' @@ -315,12 +293,12 @@ """Registration Resource. :ivar acme.messages.Registration body: - :ivar unicode new_authzr_uri: URI found in the 'next' ``Link`` header + :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') + new_authzr_uri = jose.Field('new_authzr_uri', omitempty=True) terms_of_service = jose.Field('terms_of_service', omitempty=True) @@ -425,11 +403,11 @@ """Authorization Resource. :ivar acme.messages.Authorization body: - :ivar unicode new_cert_uri: URI found in the 'next' ``Link`` header + :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') + new_cert_uri = jose.Field('new_cert_uri', omitempty=True) @Directory.register diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/acme-0.12.0/acme/messages_test.py new/acme-0.14.0/acme/messages_test.py --- old/acme-0.12.0/acme/messages_test.py 2017-03-02 23:01:28.000000000 +0100 +++ new/acme-0.14.0/acme/messages_test.py 2017-05-05 01:45:16.000000000 +0200 @@ -170,8 +170,7 @@ from acme.messages import Registration self.reg = Registration(key=key, contact=contact, agreement=agreement) - self.reg_none = Registration(authorizations='uri/authorizations', - certificates='uri/certificates') + self.reg_none = Registration() self.jobj_to = { 'contact': contact, @@ -225,14 +224,12 @@ from acme.messages import RegistrationResource self.regr = RegistrationResource( body=mock.sentinel.body, uri=mock.sentinel.uri, - new_authzr_uri=mock.sentinel.new_authzr_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, - 'new_authzr_uri': mock.sentinel.new_authzr_uri, 'terms_of_service': mock.sentinel.terms_of_service, }) @@ -346,9 +343,7 @@ from acme.messages import AuthorizationResource authzr = AuthorizationResource( uri=mock.sentinel.uri, - body=mock.sentinel.body, - new_cert_uri=mock.sentinel.new_cert_uri, - ) + body=mock.sentinel.body) self.assertTrue(isinstance(authzr, jose.JSONDeSerializable)) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/acme-0.12.0/acme/standalone.py new/acme-0.14.0/acme/standalone.py --- old/acme-0.12.0/acme/standalone.py 2017-03-02 23:01:28.000000000 +0100 +++ new/acme-0.14.0/acme/standalone.py 2017-05-05 01:45:16.000000000 +0200 @@ -6,9 +6,9 @@ import os import sys -from six.moves import BaseHTTPServer # pylint: disable=import-error +from six.moves import BaseHTTPServer # type: ignore # pylint: disable=import-error from six.moves import http_client # pylint: disable=import-error -from six.moves import socketserver # pylint: disable=import-error +from six.moves import socketserver # type: ignore # pylint: disable=import-error import OpenSSL diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/acme-0.12.0/acme/standalone_test.py new/acme-0.14.0/acme/standalone_test.py --- old/acme-0.12.0/acme/standalone_test.py 2017-03-02 23:01:28.000000000 +0100 +++ new/acme-0.14.0/acme/standalone_test.py 2017-05-05 01:45:16.000000000 +0200 @@ -7,7 +7,7 @@ import unittest from six.moves import http_client # pylint: disable=import-error -from six.moves import socketserver # pylint: disable=import-error +from six.moves import socketserver # type: ignore # pylint: disable=import-error import requests diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/acme-0.12.0/acme.egg-info/PKG-INFO new/acme-0.14.0/acme.egg-info/PKG-INFO --- old/acme-0.12.0/acme.egg-info/PKG-INFO 2017-03-02 23:01:44.000000000 +0100 +++ new/acme-0.14.0/acme.egg-info/PKG-INFO 2017-05-05 01:45:31.000000000 +0200 @@ -1,6 +1,6 @@ Metadata-Version: 1.1 Name: acme -Version: 0.12.0 +Version: 0.14.0 Summary: ACME protocol implementation in Python Home-page: https://github.com/letsencrypt/letsencrypt Author: Certbot Project @@ -19,5 +19,6 @@ Classifier: Programming Language :: Python :: 3.3 Classifier: Programming Language :: Python :: 3.4 Classifier: Programming Language :: Python :: 3.5 +Classifier: Programming Language :: Python :: 3.6 Classifier: Topic :: Internet :: WWW/HTTP Classifier: Topic :: Security diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/acme-0.12.0/acme.egg-info/requires.txt new/acme-0.14.0/acme.egg-info/requires.txt --- old/acme-0.12.0/acme.egg-info/requires.txt 2017-03-02 23:01:44.000000000 +0100 +++ new/acme-0.14.0/acme.egg-info/requires.txt 2017-05-05 01:45:31.000000000 +0200 @@ -1,11 +1,11 @@ cryptography>=0.8 +mock PyOpenSSL>=0.13 pyrfc3339 pytz requests[security]>=2.10 setuptools>=1.0 six -mock [dev] nose diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/acme-0.12.0/examples/example_client.py new/acme-0.14.0/examples/example_client.py --- old/acme-0.12.0/examples/example_client.py 2017-03-02 23:01:28.000000000 +0100 +++ new/acme-0.14.0/examples/example_client.py 2017-05-05 01:45:16.000000000 +0200 @@ -32,8 +32,7 @@ logging.debug(regr) authzr = acme.request_challenges( - identifier=messages.Identifier(typ=messages.IDENTIFIER_FQDN, value=DOMAIN), - new_authzr_uri=regr.new_authzr_uri) + identifier=messages.Identifier(typ=messages.IDENTIFIER_FQDN, value=DOMAIN)) logging.debug(authzr) authzr, authzr_response = acme.poll(authzr) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/acme-0.12.0/setup.py new/acme-0.14.0/setup.py --- old/acme-0.12.0/setup.py 2017-03-02 23:01:28.000000000 +0100 +++ new/acme-0.14.0/setup.py 2017-05-05 01:45:16.000000000 +0200 @@ -4,7 +4,7 @@ from setuptools import find_packages -version = '0.12.0' +version = '0.14.0' # Please update tox.ini when modifying dependency version requirements install_requires = [ @@ -12,6 +12,7 @@ # rsa_recover_prime_factors (>=0.8) 'cryptography>=0.8', # Connection.set_tlsext_host_name (>=0.13) + 'mock', 'PyOpenSSL>=0.13', 'pyrfc3339', 'pytz', @@ -26,16 +27,12 @@ 'six', ] -# env markers in extras_require cause problems with older pip: #517 -# Keep in sync with conditional_requirements.py. +# env markers cause problems with older pip and setuptools if sys.version_info < (2, 7): install_requires.extend([ - # only some distros recognize stdlib argparse as already satisfying 'argparse', - 'mock<1.1.0', + 'ordereddict', ]) -else: - install_requires.append('mock') dev_extras = [ 'nose', @@ -68,6 +65,7 @@ 'Programming Language :: Python :: 3.3', 'Programming Language :: Python :: 3.4', 'Programming Language :: Python :: 3.5', + 'Programming Language :: Python :: 3.6', 'Topic :: Internet :: WWW/HTTP', 'Topic :: Security', ],