Repository: certbot/certbot

Issue:
`certbot register` is swallowing error details
<img width="1460" alt="image" src="https://user-images.githubusercontent.com/311534/158033874-b5c8b03d-f8e1-4d6d-9733-5edc2fd20283.png">

Certbot is swallowing the registration error. 

One way to reproduce this is to configure Pebble to require EAB for registration. The underlying error is nowhere to be seen, unless you delve into the log files:

    certbot.errors.Error: Server requires external account binding. Please use --eab-kid and --eab-hmac-key.




Discussion:

Resolving patch:
diff --git a/certbot/certbot/_internal/main.py b/certbot/certbot/_internal/main.py
--- a/certbot/certbot/_internal/main.py
+++ b/certbot/certbot/_internal/main.py
@@ -21,6 +21,7 @@
 
 from acme import client as acme_client
 from acme import errors as acme_errors
+from acme import messages as acme_messages
 import certbot
 from certbot import configuration
 from certbot import crypto_util
@@ -726,10 +727,14 @@ def _tos_cb(terms_of_service: str) -> None:
                 display_util.notify("Account registered.")
             except errors.MissingCommandlineFlag:
                 raise
-            except errors.Error:
+            except (errors.Error, acme_messages.Error) as err:
                 logger.debug("", exc_info=True)
+                if acme_messages.is_acme_error(err):
+                    err_msg = f"Error returned by the ACME server: {str(err)}"
+                else:
+                    err_msg = str(err)
                 raise errors.Error(
-                    "Unable to register an account with ACME server")
+                    f"Unable to register an account with ACME server. {err_msg}")
 
     config.account = acc.id
     return acc, acme


Test patch:
diff --git a/certbot/tests/main_test.py b/certbot/tests/main_test.py
--- a/certbot/tests/main_test.py
+++ b/certbot/tests/main_test.py
@@ -16,6 +16,7 @@
 import josepy as jose
 import pytz
 
+from acme.messages import Error as acme_error
 from certbot import crypto_util, configuration
 from certbot import errors
 from certbot import interfaces
@@ -593,6 +594,17 @@ def _call(self):
             mock_storage.return_value = self.account_storage
             return _determine_account(self.config)
 
+    @mock.patch('certbot._internal.client.register')
+    @mock.patch('certbot._internal.client.display_ops.get_email')
+    def _register_error_common(self, err_msg, exception, mock_get_email, mock_register):
+        mock_get_email.return_value = 'foo@bar.baz'
+        mock_register.side_effect = exception
+        try:
+            self._call()
+        except errors.Error as err:
+            self.assertEqual(f"Unable to register an account with ACME server. {err_msg}",
+                             str(err))
+
     def test_args_account_set(self):
         self.account_storage.save(self.accs[1], self.mock_client)
         self.config.account = self.accs[1].id
@@ -617,6 +629,16 @@ def test_multiple_accounts(self, mock_choose_accounts):
         self.assertEqual(self.accs[1].id, self.config.account)
         self.assertIsNone(self.config.email)
 
+    @mock.patch('certbot._internal.client.display_ops.choose_account')
+    def test_multiple_accounts_canceled(self, mock_choose_accounts):
+        for acc in self.accs:
+            self.account_storage.save(acc, self.mock_client)
+        mock_choose_accounts.return_value = None
+        try:
+            self._call()
+        except errors.Error as err:
+            self.assertIn("No account has been chosen", str(err))
+
     @mock.patch('certbot._internal.client.display_ops.get_email')
     @mock.patch('certbot._internal.main.display_util.notify')
     def test_no_accounts_no_email(self, mock_notify, mock_get_email):
@@ -641,6 +663,24 @@ def test_no_accounts_email(self):
         self.assertEqual(self.accs[1].id, self.config.account)
         self.assertEqual('other email', self.config.email)
 
+    def test_register_error_certbot(self):
+        err_msg = "Some error message raised by Certbot"
+        self._register_error_common(err_msg, errors.Error(err_msg))
+
+    def test_register_error_acme_type_and_detail(self):
+        err_msg = ("Error returned by the ACME server: urn:ietf:params:acme:"
+                   "error:malformed :: The request message was malformed :: "
+                   "must agree to terms of service")
+        exception = acme_error(typ = "urn:ietf:params:acme:error:malformed",
+                               detail = "must agree to terms of service")
+        self._register_error_common(err_msg, exception)
+
+    def test_register_error_acme_type_only(self):
+        err_msg = ("Error returned by the ACME server: urn:ietf:params:acme:"
+                   "error:serverInternal :: The server experienced an internal error")
+        exception = acme_error(typ = "urn:ietf:params:acme:error:serverInternal")
+        self._register_error_common(err_msg, exception)
+
 
 class MainTest(test_util.ConfigTestCase):
     """Tests for different commands."""