Repository: certbot/certbot

Issue:
`show_account` broken in combination with ACMEv1 account reuse using symbolic links
Reported on the Community here: https://community.letsencrypt.org/t/certbot-show-account-fails-with-acme-v01-api-letsencrypt-org-dns-lookup-failure/178367

Since #6156 ACMEv1 account reuse is done using symbolic links. 

In #9127 the `show_account` subcommand was introduced, making use of the [`query_registration`](https://github.com/certbot/certbot/blob/5c111d0bd1206d864d7cb93754e101f6073bc669/acme/acme/client.py#L641-L655) function of the `acme` library, passing the `regr` object from the account to that function.

However, when symbolic link account reuse is in place, the `regr` object contains an account `uri` of the ACMEv1 protocol, including the now-nonexisting `acme-v01` API URL. That URI is directly used in [`_send_recv_regr()`](https://github.com/certbot/certbot/blob/5c111d0bd1206d864d7cb93754e101f6073bc669/acme/acme/client.py#L84), triggering a DNS error.

Possible fixes:

* Detect ACMEv1 account URI and translate it into an ACMEv2 account URI *in Certbots `main.show_account`* and keep using `query_registration`, or
* Detect ACMEv1 account URI and translate it into an ACMEv2 account URI *in acme's `client.query_registration`*, or
* Rewrite Certbots `main.show_account()` to *not* use `query_registration` but build a signed request for `newAccount` on its own.


Discussion:
Assuming that the `acme` module makes it possible, I prefer the `newAccount` option. 

However, in the past I noticed that the possibility to do this is not part of the public API: see `acme.ClientV2._get_v2_account`. Neither `new_account` nor `query_registration` fits the bill for what we want to do. 

I am wondering out loud: why we can't change the implementation of `query_registration` to call `_get_v2_account`?

Resolving patch:
diff --git a/acme/acme/client.py b/acme/acme/client.py
--- a/acme/acme/client.py
+++ b/acme/acme/client.py
@@ -646,12 +646,8 @@ def query_registration(self, regr: messages.RegistrationResource
             Resource.
 
         """
-        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)
+        self.net.account = self._get_v2_account(regr, True)
+
         return self.net.account
 
     def update_registration(self, regr: messages.RegistrationResource,
@@ -671,12 +667,15 @@ def update_registration(self, regr: messages.RegistrationResource,
         new_regr = self._get_v2_account(regr)
         return super().update_registration(new_regr, update)
 
-    def _get_v2_account(self, regr: messages.RegistrationResource) -> messages.RegistrationResource:
+    def _get_v2_account(self, regr: messages.RegistrationResource, update_body: bool = False
+                       ) -> messages.RegistrationResource:
         self.net.account = None
         only_existing_reg = regr.body.update(only_return_existing=True)
         response = self._post(self.directory['newAccount'], only_existing_reg)
         updated_uri = response.headers['Location']
-        new_regr = regr.update(uri=updated_uri)
+        new_regr = regr.update(body=messages.Registration.from_json(response.json())
+                               if update_body else regr.body,
+                               uri=updated_uri)
         self.net.account = new_regr
         return new_regr
 


Test patch:
diff --git a/acme/tests/client_test.py b/acme/tests/client_test.py
--- a/acme/tests/client_test.py
+++ b/acme/tests/client_test.py
@@ -140,6 +140,7 @@ def test_query_registration_client_v2(self):
         self.response.json.return_value = DIRECTORY_V2.to_json()
         client = self._init()
         self.response.json.return_value = self.regr.body.to_json()
+        self.response.headers = {'Location': 'https://www.letsencrypt-demo.org/acme/reg/1'}
         self.assertEqual(self.regr, client.query_registration(self.regr))
 
     def test_forwarding(self):