Package: release.debian.org
Severity: normal
Tags: stretch
User: release.debian....@packages.debian.org
Usertags: pu

Hello release managers,

We have a proposed update for acme in stretch (oldstable).  This is
necessary to prevent the package, and all its dependencies, stopping
to work due to changes to the web service that the acme module is
primarily used for.  (Let's Encrypt)

We've backported the minimal set of patches, and upstream has
validated that the set is correct.

-- System Information:
Debian Release: bullseye/sid
  APT prefers testing
  APT policy: (900, 'testing')
Architecture: amd64 (x86_64)
Foreign Architectures: i386

Kernel: Linux 5.2.0-2-amd64 (SMP w/4 CPU cores)
Locale: LANG=en_US.UTF-8, LC_CTYPE=en_US.UTF-8 (charmap=UTF-8), 
LANGUAGE=en_US.UTF-8 (charmap=UTF-8)
Shell: /bin/sh linked to /bin/dash
Init: systemd (via /run/systemd/system)
LSM: AppArmor: enabled
diff -Nru python-acme-0.28.0/debian/changelog 
python-acme-0.28.0/debian/changelog
--- python-acme-0.28.0/debian/changelog 2018-12-02 16:24:15.000000000 -0500
+++ python-acme-0.28.0/debian/changelog 2019-07-31 22:26:45.000000000 -0400
@@ -1,3 +1,11 @@
+python-acme (0.28.0-1~deb9u2) stretch; urgency=medium
+
+  * This stretch update is to switch to using a POST-as-GET protocol
+    before the November 1, 2019 deadline when Let's Encrypt will begin
+    refusing requests using the (old) GET protocol. (Closes: #932248)
+
+ -- Harlan Lieberman-Berg <hlieber...@debian.org>  Wed, 31 Jul 2019 22:26:45 
-0400
+
 python-acme (0.28.0-1~deb9u1) stretch; urgency=medium
 
   * This stretch update is to cure the problem caused by the deprecation
diff -Nru python-acme-0.28.0/debian/patches/0000-post-as-get.patch 
python-acme-0.28.0/debian/patches/0000-post-as-get.patch
--- python-acme-0.28.0/debian/patches/0000-post-as-get.patch    1969-12-31 
19:00:00.000000000 -0500
+++ python-acme-0.28.0/debian/patches/0000-post-as-get.patch    2019-07-31 
18:40:59.000000000 -0400
@@ -0,0 +1,317 @@
+From 0b5468e992ab57fa028ddf33ca2351cb37c8e1ee Mon Sep 17 00:00:00 2001
+From: Adrien Ferrand <adferr...@users.noreply.github.com>
+Date: Fri, 30 Nov 2018 01:42:06 +0100
+Subject: [PATCH] Implement POST-as-GET requests (#6522)
+
+* Setup an integration tests env against Pebble, that enforce post-as-get
+
+* Implement POST-as-GET requests, with fallback to GET.
+
+* Fix unit tests
+
+* Fix coverage.
+
+* Fix or ignore lint errors
+
+* Corrections after review
+
+* Correct test
+
+* Try a simple delegate approach
+
+* Add a test
+
+* Simplify test mocking
+
+* Clean comment
+---
+Index: python-acme/acme/client.py
+===================================================================
+--- python-acme.orig/acme/client.py
++++ python-acme/acme/client.py
+@@ -199,22 +199,6 @@ class ClientBase(object):  # pylint: dis
+ 
+         return datetime.datetime.now() + datetime.timedelta(seconds=seconds)
+ 
+-    def poll(self, authzr):
+-        """Poll Authorization Resource for status.
+-
+-        :param authzr: Authorization Resource
+-        :type authzr: `.AuthorizationResource`
+-
+-        :returns: Updated Authorization Resource and HTTP response.
+-
+-        :rtype: (`.AuthorizationResource`, `requests.Response`)
+-
+-        """
+-        response = self.net.get(authzr.uri)
+-        updated_authzr = self._authzr_from_response(
+-            response, authzr.body.identifier, authzr.uri)
+-        return updated_authzr, response
+-
+     def _revoke(self, cert, rsn, url):
+         """Revoke certificate.
+ 
+@@ -236,6 +220,7 @@ class ClientBase(object):  # pylint: dis
+             raise errors.ClientError(
+                 'Successful revocation must return HTTP OK status')
+ 
++
+ class Client(ClientBase):
+     """ACME client for a v1 API.
+ 
+@@ -388,6 +373,22 @@ class Client(ClientBase):
+             body=jose.ComparableX509(OpenSSL.crypto.load_certificate(
+                 OpenSSL.crypto.FILETYPE_ASN1, response.content)))
+ 
++    def poll(self, authzr):
++        """Poll Authorization Resource for status.
++
++        :param authzr: Authorization Resource
++        :type authzr: `.AuthorizationResource`
++
++        :returns: Updated Authorization Resource and HTTP response.
++
++        :rtype: (`.AuthorizationResource`, `requests.Response`)
++
++        """
++        response = self.net.get(authzr.uri)
++        updated_authzr = self._authzr_from_response(
++            response, authzr.body.identifier, authzr.uri)
++        return updated_authzr, response
++
+     def poll_and_request_issuance(
+             self, csr, authzrs, mintime=5, max_attempts=10):
+         """Poll and request issuance.
+@@ -651,13 +652,29 @@ class ClientV2(ClientBase):
+         body = messages.Order.from_json(response.json())
+         authorizations = []
+         for url in body.authorizations:
+-            
authorizations.append(self._authzr_from_response(self.net.get(url), uri=url))
++            
authorizations.append(self._authzr_from_response(self._post_as_get(url), 
uri=url))
+         return messages.OrderResource(
+             body=body,
+             uri=response.headers.get('Location'),
+             authorizations=authorizations,
+             csr_pem=csr_pem)
+ 
++    def poll(self, authzr):
++        """Poll Authorization Resource for status.
++
++        :param authzr: Authorization Resource
++        :type authzr: `.AuthorizationResource`
++
++        :returns: Updated Authorization Resource and HTTP response.
++
++        :rtype: (`.AuthorizationResource`, `requests.Response`)
++
++        """
++        response = self._post_as_get(authzr.uri)
++        updated_authzr = self._authzr_from_response(
++            response, authzr.body.identifier, authzr.uri)
++        return updated_authzr, response
++
+     def poll_and_finalize(self, orderr, deadline=None):
+         """Poll authorizations and finalize the order.
+ 
+@@ -681,7 +698,7 @@ class ClientV2(ClientBase):
+         responses = []
+         for url in orderr.body.authorizations:
+             while datetime.datetime.now() < deadline:
+-                authzr = self._authzr_from_response(self.net.get(url), 
uri=url)
++                authzr = self._authzr_from_response(self._post_as_get(url), 
uri=url)
+                 if authzr.body.status != messages.STATUS_PENDING:
+                     responses.append(authzr)
+                     break
+@@ -716,12 +733,12 @@ class ClientV2(ClientBase):
+         self._post(orderr.body.finalize, wrapped_csr)
+         while datetime.datetime.now() < deadline:
+             time.sleep(1)
+-            response = self.net.get(orderr.uri)
++            response = self._post_as_get(orderr.uri)
+             body = messages.Order.from_json(response.json())
+             if body.error is not None:
+                 raise errors.IssuanceError(body.error)
+             if body.certificate is not None:
+-                certificate_response = self.net.get(body.certificate,
++                certificate_response = self._post_as_get(body.certificate,
+                                                     
content_type=DER_CONTENT_TYPE).text
+                 return orderr.update(body=body, 
fullchain_pem=certificate_response)
+         raise errors.TimeoutError()
+@@ -739,6 +756,32 @@ class ClientV2(ClientBase):
+         """
+         return self._revoke(cert, rsn, self.directory['revokeCert'])
+ 
++    def _post_as_get(self, *args, **kwargs):
++        """
++        Send GET request using the POST-as-GET protocol if needed.
++        The request will be first issued using POST-as-GET for ACME v2. If 
the ACME CA servers do
++        not support this yet and return an error, request will be retried 
using GET.
++        For ACME v1, only GET request will be tried, as POST-as-GET is not 
supported.
++        :param args:
++        :param kwargs:
++        :return:
++        """
++        if self.acme_version >= 2:
++            # We add an empty payload for POST-as-GET requests
++            new_args = args[:1] + (None,) + args[1:]
++            try:
++                return self._post(*new_args, **kwargs)  # pylint: 
disable=star-args
++            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)
++                    logger.debug('Retrying request with GET.')
++                else:  # pragma: no cover
++                    raise
++
++        # If POST-as-GET is not supported yet, we use a GET instead.
++        return self.net.get(*args, **kwargs)
++
+ 
+ class BackwardsCompatibleClientV2(object):
+     """ACME client wrapper that tends towards V2-style calls, but
+@@ -768,12 +811,7 @@ class BackwardsCompatibleClientV2(object
+             self.client = ClientV2(directory, net=net)
+ 
+     def __getattr__(self, name):
+-        if name in vars(self.client):
+-            return getattr(self.client, name)
+-        elif name in dir(ClientBase):
+-            return getattr(self.client, name)
+-        else:
+-            raise AttributeError()
++        return getattr(self.client, name)
+ 
+     def new_account_and_tos(self, regr, check_tos_cb=None):
+         """Combined register and agree_tos for V1, new_account for V2
+@@ -943,7 +981,7 @@ class ClientNetwork(object):  # pylint:
+         :rtype: `josepy.JWS`
+ 
+         """
+-        jobj = obj.json_dumps(indent=2).encode()
++        jobj = obj.json_dumps(indent=2).encode() if obj else b''
+         logger.debug('JWS payload:\n%s', jobj)
+         kwargs = {
+             "alg": self.alg,
+Index: python-acme/acme/client_test.py
+===================================================================
+--- python-acme.orig/acme/client_test.py
++++ python-acme/acme/client_test.py
+@@ -1,4 +1,5 @@
+ """Tests for acme.client."""
++# pylint: disable=too-many-lines
+ import copy
+ import datetime
+ import json
+@@ -730,9 +731,10 @@ class ClientV2Test(ClientTestBase):
+         authz_response2 = self.response
+         authz_response2.json.return_value = self.authz2.to_json()
+         authz_response2.headers['Location'] = self.authzr2.uri
+-        self.net.get.side_effect = (authz_response, authz_response2)
+ 
+-        self.assertEqual(self.client.new_order(CSR_SAN_PEM), self.orderr)
++        with mock.patch('acme.client.ClientV2._post_as_get') as 
mock_post_as_get:
++            mock_post_as_get.side_effect = (authz_response, authz_response2)
++            self.assertEqual(self.client.new_order(CSR_SAN_PEM), self.orderr)
+ 
+     @mock.patch('acme.client.datetime')
+     def test_poll_and_finalize(self, mock_datetime):
+@@ -821,6 +823,30 @@ class ClientV2Test(ClientTestBase):
+         self.response.json.return_value = self.regr.body.update(
+             contact=()).to_json()
+ 
++    def test_post_as_get(self):
++        with mock.patch('acme.client.ClientV2._authzr_from_response') as 
mock_client:
++            mock_client.return_value = self.authzr2
++
++            self.client.poll(self.authzr2)  # pylint: disable=protected-access
++
++            self.client.net.post.assert_called_once_with(
++                self.authzr2.uri, None, acme_version=2,
++                
new_nonce_url='https://www.letsencrypt-demo.org/acme/new-nonce')
++            self.client.net.get.assert_not_called()
++
++            class FakeError(messages.Error):  # pylint: 
disable=too-many-ancestors
++                """Fake error to reproduce a malformed request ACME error"""
++                def __init__(self):  # pylint: disable=super-init-not-called
++                    pass
++                @property
++                def code(self):
++                    return 'malformed'
++            self.client.net.post.side_effect = FakeError()
++
++            self.client.poll(self.authzr2)  # pylint: disable=protected-access
++
++            self.client.net.get.assert_called_once_with(self.authzr2.uri)
++
+ 
+ class MockJSONDeSerializable(jose.JSONDeSerializable):
+     # pylint: disable=missing-docstring
+Index: python-acme/tests/certbot-pebble-integration.sh
+===================================================================
+--- /dev/null
++++ python-acme/tests/certbot-pebble-integration.sh
+@@ -0,0 +1,16 @@
++#!/bin/bash
++# Simple integration test. Make sure to activate virtualenv beforehand
++# (source venv/bin/activate) and that you are running Pebble test
++# instance (see ./pebble-fetch.sh).
++
++cleanup_and_exit() {
++    EXIT_STATUS=$?
++    unset SERVER
++    exit $EXIT_STATUS
++}
++
++trap cleanup_and_exit EXIT
++
++export SERVER=https://localhost:14000/dir
++
++./tests/certbot-boulder-integration.sh
+Index: python-acme/tests/pebble-fetch.sh
+===================================================================
+--- /dev/null
++++ python-acme/tests/pebble-fetch.sh
+@@ -0,0 +1,41 @@
++#!/bin/bash
++# Download and run Pebble instance for integration testing
++set -xe
++
++PEBBLE_VERSION=2018-11-02
++
++# We reuse the same GOPATH-style directory than for Boulder.
++# Pebble does not need it, but it will make the installation consistent with 
Boulder's one.
++export GOPATH=${GOPATH:-$HOME/gopath}
++PEBBLEPATH=${PEBBLEPATH:-$GOPATH/src/github.com/letsencrypt/pebble}
++
++mkdir -p ${PEBBLEPATH}
++
++cat << UNLIKELY_EOF > "$PEBBLEPATH/docker-compose.yml"
++version: '3'
++
++services:
++ pebble:
++  image: letsencrypt/pebble:${PEBBLE_VERSION}
++  command: pebble -strict ${PEBBLE_STRICT:-false} -dnsserver 10.77.77.1
++  ports:
++    - 14000:14000
++  environment:
++    - PEBBLE_VA_NOSLEEP=1
++UNLIKELY_EOF
++
++docker-compose -f "$PEBBLEPATH/docker-compose.yml" up -d pebble
++
++set +x  # reduce verbosity while waiting for boulder
++for n in `seq 1 150` ; do
++  if curl -k https://localhost:14000/dir 2>/dev/null; then
++    break
++  else
++    sleep 1
++  fi
++done
++
++if ! curl -k https://localhost:14000/dir 2>/dev/null; then
++  echo "timed out waiting for pebble to start"
++  exit 1
++fi
diff -Nru python-acme-0.28.0/debian/patches/0001-post-as-get.patch 
python-acme-0.28.0/debian/patches/0001-post-as-get.patch
--- python-acme-0.28.0/debian/patches/0001-post-as-get.patch    1969-12-31 
19:00:00.000000000 -0500
+++ python-acme-0.28.0/debian/patches/0001-post-as-get.patch    2019-07-31 
18:41:01.000000000 -0400
@@ -0,0 +1,66 @@
+From b0d960f102c998d8231c0ee48952b488f10864ac Mon Sep 17 00:00:00 2001
+From: Adrien Ferrand <adferr...@users.noreply.github.com>
+Date: Wed, 1 May 2019 00:37:23 +0200
+Subject: [PATCH] Send a POST-as-GET request to query registration in ACME v2
+ (#6993)
+
+* Send a post-as-get request to query registration
+
+* Add comments. Add again a line.
+
+* Prepare code for future PR about post-as-get
+---
+Index: python-acme/acme/client.py
+===================================================================
+--- python-acme.orig/acme/client.py
++++ python-acme/acme/client.py
+@@ -122,15 +122,6 @@ class ClientBase(object):  # pylint: dis
+         """
+         return self.update_registration(regr, update={'status': 
'deactivated'})
+ 
+-    def query_registration(self, regr):
+-        """Query server about registration.
+-
+-        :param messages.RegistrationResource: Existing Registration
+-            Resource.
+-
+-        """
+-        return self._send_recv_regr(regr, messages.UpdateRegistration())
+-
+     def _authzr_from_response(self, response, identifier=None, uri=None):
+         authzr = messages.AuthorizationResource(
+             body=messages.Authorization.from_json(response.json()),
+@@ -275,6 +266,15 @@ class Client(ClientBase):
+         # pylint: disable=no-member
+         return self._regr_from_response(response)
+ 
++    def query_registration(self, regr):
++        """Query server about registration.
++
++        :param messages.RegistrationResource: Existing Registration
++            Resource.
++
++        """
++        return self._send_recv_regr(regr, messages.UpdateRegistration())
++
+     def agree_to_tos(self, regr):
+         """Agree to the terms-of-service.
+ 
+@@ -602,10 +602,13 @@ class ClientV2(ClientBase):
+             Resource.
+ 
+         """
+-        self.net.account = regr
+-        updated_regr = super(ClientV2, self).query_registration(regr)
+-        self.net.account = updated_regr
+-        return updated_regr
++        self.net.account = regr  # See certbot/certbot#6258
++        # ACME v2 requires to use a POST-as-GET request (POST an empty JWS) 
here.
++        # This is done by passing None instead of an empty UpdateRegistration 
to _post().
++        response = self._post(regr.uri, None)
++        self.net.account = self._regr_from_response(response, uri=regr.uri,
++                                                    
terms_of_service=regr.terms_of_service)
++        return self.net.account
+ 
+     def update_registration(self, regr, update=None):
+         """Update registration.
diff -Nru python-acme-0.28.0/debian/patches/0002-post-as-get.patch 
python-acme-0.28.0/debian/patches/0002-post-as-get.patch
--- python-acme-0.28.0/debian/patches/0002-post-as-get.patch    1969-12-31 
19:00:00.000000000 -0500
+++ python-acme-0.28.0/debian/patches/0002-post-as-get.patch    2019-07-31 
18:41:03.000000000 -0400
@@ -0,0 +1,24 @@
+From a0a8292ff26a2d062e75b865d9b9b10977dc1f80 Mon Sep 17 00:00:00 2001
+From: Adrien Ferrand <adferr...@users.noreply.github.com>
+Date: Wed, 13 Feb 2019 00:36:27 +0100
+Subject: [PATCH] Correct the Content-Type used in the POST-as-GET request to
+ retrieve a cert (#6757)
+
+---
+ acme/client.py | 3 +--
+ 1 file changed, 1 insertion(+), 2 deletions(-)
+
+Index: python-acme/acme/client.py
+===================================================================
+--- python-acme.orig/acme/client.py
++++ python-acme/acme/client.py
+@@ -741,8 +741,7 @@ class ClientV2(ClientBase):
+             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()
+ 
diff -Nru python-acme-0.28.0/debian/patches/0003-remove-keyauth-from-jws.patch 
python-acme-0.28.0/debian/patches/0003-remove-keyauth-from-jws.patch
--- python-acme-0.28.0/debian/patches/0003-remove-keyauth-from-jws.patch        
1969-12-31 19:00:00.000000000 -0500
+++ python-acme-0.28.0/debian/patches/0003-remove-keyauth-from-jws.patch        
2019-07-31 22:26:45.000000000 -0400
@@ -0,0 +1,209 @@
+From 339d034d6a5a57d296607795a4706203f81d7059 Mon Sep 17 00:00:00 2001
+From: Adrien Ferrand <adferr...@users.noreply.github.com>
+Date: Wed, 27 Feb 2019 18:21:47 +0100
+Subject: [PATCH] Remove keyAuthorization field from the challenge response JWS
+ token (#6758)
+
+Fixes #6755.
+
+POSTing the `keyAuthorization` in a JWS token when answering an ACME 
challenge, has been deprecated for some time now. Indeed, this is superfluous 
as the request is already authentified by the JWS signature.
+
+Boulder still accepts to see this field in the JWS token, and ignore it. 
Pebble in non strict mode also. But Pebble in strict mode refuses the request, 
to prepare complete removal of this field in ACME v2.
+
+Certbot still sends the `keyAuthorization` field. This PR removes it, and 
makes Certbot compliant with current ACME v2 protocol, and so Pebble in strict 
mode.
+
+See also 
[letsencrypt/pebble#192](https://github.com/letsencrypt/pebble/issues/192) for 
implementation details server side.
+
+* New implementation, with a fallback.
+
+* Update acme/acme/client.py
+
+Co-Authored-By: adferrand <adferr...@users.noreply.github.com>
+
+* Fix an instance parameter
+
+* Update comment
+
+* Add unit tests on keyAuthorization dump
+
+* Update acme/client.py
+
+Co-Authored-By: adferrand <adferr...@users.noreply.github.com>
+
+* Restrict the magic of setting a variable in immutable object in one place. 
Make a soon to be removed method private.
+---
+ acme/challenges.py      | 20 ++++++++++++++++++++
+ acme/challenges_test.py | 12 ++++++++++++
+ acme/client.py          | 26 ++++++++++++++++++++------
+ acme/client_test.py     | 28 ++++++++++++++++++++++++++++
+ 4 files changed, 87 insertions(+), 6 deletions(-)
+
+Index: python-acme/acme/challenges.py
+===================================================================
+--- python-acme.orig/acme/challenges.py
++++ python-acme/acme/challenges.py
+@@ -108,6 +108,10 @@ class KeyAuthorizationChallengeResponse(
+     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 @@ class KeyAuthorizationChallengeResponse(
+
+         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):
+Index: python-acme/acme/challenges_test.py
+===================================================================
+--- python-acme.orig/acme/challenges_test.py
++++ python-acme/acme/challenges_test.py
+@@ -94,6 +94,9 @@ class DNS01ResponseTest(unittest.TestCas
+         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 @@ class HTTP01ResponseTest(unittest.TestCa
+         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 @@ class TLSSNI01ResponseTest(unittest.Test
+         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):
+Index: python-acme/acme/client.py
+===================================================================
+--- python-acme.orig/acme/client.py
++++ python-acme/acme/client.py
+@@ -17,6 +17,7 @@ import requests
+ from requests.adapters import HTTPAdapter
+ import sys
+
++from acme import challenges
+ from acme import crypto_util
+ from acme import errors
+ from acme import jws
+@@ -145,7 +146,23 @@ class ClientBase(object):  # pylint: dis
+         :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:
+@@ -776,7 +793,7 @@ class ClientV2(ClientBase):
+             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
+@@ -1177,10 +1194,7 @@ class ClientNetwork(object):  # pylint:
+
+     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)
+Index: python-acme/acme/client_test.py
+===================================================================
+--- python-acme.orig/acme/client_test.py
++++ python-acme/acme/client_test.py
+@@ -432,6 +432,34 @@ class ClientTest(ClientTestBase):
+             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 -Nru python-acme-0.28.0/debian/patches/series 
python-acme-0.28.0/debian/patches/series
--- python-acme-0.28.0/debian/patches/series    2018-12-02 16:24:15.000000000 
-0500
+++ python-acme-0.28.0/debian/patches/series    2019-07-31 18:39:45.000000000 
-0400
@@ -1,2 +1,6 @@
 e3cb782e5992ba306de59ba96dfb6f125720cd06.patch
 ec297ccf72e95961586ec2382c3e3225ce578aa4.patch
+0000-post-as-get.patch
+0001-post-as-get.patch
+0002-post-as-get.patch
+0003-remove-keyauth-from-jws.patch
diff -Nru python-acme-0.28.0/debian/rules python-acme-0.28.0/debian/rules
--- python-acme-0.28.0/debian/rules     2018-05-26 13:55:06.000000000 -0400
+++ python-acme-0.28.0/debian/rules     2019-07-31 22:26:45.000000000 -0400
@@ -15,3 +15,9 @@
 override_dh_auto_install:
        dh_auto_install
        find $(CURDIR)/debian/ -type d -name testdata -print0 | xargs -0 rm -rf 
'{}' \;
+
+override_dh_auto_test:
+ifeq (,$(filter nocheck,$(DEB_BUILD_OPTIONS)))
+       python2 setup.py test
+       python3 setup.py test
+endif

Reply via email to