Repository: certbot/certbot

Issue:
Implement acme-spec#134: "Shift URIs around"
Client should implement https://github.com/letsencrypt/acme-spec/pull/134.

I'm almost finished with a relevant PR, but it's blocked on https://github.com/letsencrypt/boulder/issues/423.



Discussion:

Resolving patch:
diff --git a/acme/acme/client.py b/acme/acme/client.py
--- a/acme/acme/client.py
+++ b/acme/acme/client.py
@@ -94,18 +94,8 @@ def register(self, new_reg=None):
 
         return regr
 
-    def update_registration(self, regr):
-        """Update registration.
-
-        :pram regr: Registration Resource.
-        :type regr: `.RegistrationResource`
-
-        :returns: Updated Registration Resource.
-        :rtype: `.RegistrationResource`
-
-        """
-        response = self.net.post(
-            regr.uri, messages.UpdateRegistration(**dict(regr.body)))
+    def _send_recv_regr(self, regr, body):
+        response = self.net.post(regr.uri, body)
 
         # TODO: Boulder returns httplib.ACCEPTED
         #assert response.status_code == httplib.OK
@@ -113,13 +103,37 @@ def update_registration(self, regr):
         # TODO: Boulder does not set Location or Link on update
         # (c.f. acme-spec #94)
 
-        updated_regr = self._regr_from_response(
+        return self._regr_from_response(
             response, uri=regr.uri, new_authzr_uri=regr.new_authzr_uri,
             terms_of_service=regr.terms_of_service)
+
+    def update_registration(self, regr, update=None):
+        """Update registration.
+
+        :param messages.RegistrationResource regr: Registration Resource.
+        :param messages.Registration update: Updated body of the
+            resource. If not provided, body will be taken from `regr`.
+
+        :returns: Updated Registration Resource.
+        :rtype: `.RegistrationResource`
+
+        """
+        update = regr.body if update is None else update
+        updated_regr = self._send_recv_regr(
+            regr, body=messages.UpdateRegistration(**dict(update)))
         if updated_regr != regr:
             raise errors.UnexpectedUpdate(regr)
         return updated_regr
 
+    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.
 
@@ -275,8 +289,7 @@ def request_issuance(self, csr, authzrs):
         logger.debug("Requesting issuance...")
 
         # TODO: assert len(authzrs) == number of SANs
-        req = messages.CertificateRequest(
-            csr=csr, authorizations=tuple(authzr.uri for authzr in authzrs))
+        req = messages.CertificateRequest(csr=csr)
 
         content_type = self.DER_CONTENT_TYPE  # TODO: add 'cert_type 'argument
         response = self.net.post(
diff --git a/acme/acme/messages.py b/acme/acme/messages.py
--- a/acme/acme/messages.py
+++ b/acme/acme/messages.py
@@ -157,6 +157,10 @@ class Registration(ResourceBody):
     :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
@@ -164,6 +168,24 @@ class Registration(ResourceBody):
     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)
+
+    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:'
@@ -327,13 +349,11 @@ class CertificateRequest(jose.JSONObjectWithFields):
 
     :ivar acme.jose.util.ComparableX509 csr:
         `OpenSSL.crypto.X509Req` wrapped in `.ComparableX509`
-    :ivar tuple authorizations: `tuple` of URIs (`str`)
 
     """
     resource_type = 'new-cert'
     resource = fields.Resource(resource_type)
     csr = jose.Field('csr', decoder=jose.decode_csr, encoder=jose.encode_csr)
-    authorizations = jose.Field('authorizations', decoder=tuple)
 
 
 class CertificateResource(ResourceWithURI):


Test patch:
diff --git a/acme/acme/client_test.py b/acme/acme/client_test.py
--- a/acme/acme/client_test.py
+++ b/acme/acme/client_test.py
@@ -111,6 +111,10 @@ def test_update_registration(self):
         self.assertRaises(
             errors.UnexpectedUpdate, self.client.update_registration, self.regr)
 
+    def test_query_registration(self):
+        self.response.json.return_value = self.regr.body.to_json()
+        self.assertEqual(self.regr, self.client.query_registration(self.regr))
+
     def test_agree_to_tos(self):
         self.client.update_registration = mock.Mock()
         self.client.agree_to_tos(self.regr)
diff --git a/acme/acme/messages_test.py b/acme/acme/messages_test.py
--- a/acme/acme/messages_test.py
+++ b/acme/acme/messages_test.py
@@ -105,7 +105,8 @@ def setUp(self):
 
         from acme.messages import Registration
         self.reg = Registration(key=key, contact=contact, agreement=agreement)
-        self.reg_none = Registration()
+        self.reg_none = Registration(authorizations='uri/authorizations',
+                                     certificates='uri/certificates')
 
         self.jobj_to = {
             'contact': contact,
@@ -141,6 +142,17 @@ def test_from_json_hashable(self):
         hash(Registration.from_json(self.jobj_from))
 
 
+class UpdateRegistrationTest(unittest.TestCase):
+    """Tests for acme.messages.UpdateRegistration."""
+
+    def test_empty(self):
+        from acme.messages import UpdateRegistration
+        jstring = '{"resource": "reg"}'
+        self.assertEqual(jstring, UpdateRegistration().json_dumps())
+        self.assertEqual(
+            UpdateRegistration(), UpdateRegistration.json_loads(jstring))
+
+
 class RegistrationResourceTest(unittest.TestCase):
     """Tests for acme.messages.RegistrationResource."""
 
@@ -280,7 +292,7 @@ class CertificateRequestTest(unittest.TestCase):
 
     def setUp(self):
         from acme.messages import CertificateRequest
-        self.req = CertificateRequest(csr=CSR, authorizations=('foo',))
+        self.req = CertificateRequest(csr=CSR)
 
     def test_json_de_serializable(self):
         self.assertTrue(isinstance(self.req, jose.JSONDeSerializable))