Repository: certbot/certbot

Issue:
Certbot should stop querying when order status is invalid
If you're having trouble using Certbot and aren't sure you've found a bug or
request for a new feature, please first try asking for help at
https://community.letsencrypt.org/. There is a much larger community there of
people familiar with the project who will be able to more quickly answer your
questions.

## My operating system is (include version):
Ubuntu 20.04

## I installed Certbot with (snap, OS package manager, pip, certbot-auto, etc):


## I ran this command and it produced this output:
sudo certbot certonly --server *** --domain *** --preferred-challenges=http --manual


## Certbot's behavior differed from what I expected because:
In RFC8555: [7.1.6](https://datatracker.ietf.org/doc/html/rfc8555#section-7.1.6).  Status Changes
When getOrder action returns invalid, certbot should stop querying.
However certbot continue querying getOrder url until timeout.

## Here is a Certbot log showing the issue (if available):
###### Logs are stored in `/var/log/letsencrypt` by default. Feel free to redact domains, e-mail and IP addresses as you see fit.

## Here is the relevant nginx server block or Apache virtualhost for the domain I am configuring:



Discussion:
>  When getOrder action returns invalid, certbot should stop querying.
> However certbot continue querying getOrder url until timeout.

To clarify the scenario, the ACME server is setting the order `status` to `invalid` during finalization, but the `error` field is not set?

Resolving patch:
diff --git a/acme/acme/client.py b/acme/acme/client.py
--- a/acme/acme/client.py
+++ b/acme/acme/client.py
@@ -797,9 +797,13 @@ def finalize_order(self, orderr: messages.OrderResource, deadline: datetime.date
             time.sleep(1)
             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:
+            if body.status == messages.STATUS_INVALID:
+                if body.error is not None:
+                    raise errors.IssuanceError(body.error)
+                raise errors.Error(
+                    "The certificate order failed. No further information was provided "
+                    "by the server.")
+            elif body.status == messages.STATUS_VALID and body.certificate is not None:
                 certificate_response = self._post_as_get(body.certificate)
                 orderr = orderr.update(body=body, fullchain_pem=certificate_response.text)
                 if fetch_alternative_chains:


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
@@ -822,7 +822,8 @@ def test_poll_authorizations_success(self):
 
     def test_finalize_order_success(self):
         updated_order = self.order.update(
-            certificate='https://www.letsencrypt-demo.org/acme/cert/')
+            certificate='https://www.letsencrypt-demo.org/acme/cert/',
+            status=messages.STATUS_VALID)
         updated_orderr = self.orderr.update(body=updated_order, fullchain_pem=CERT_SAN_PEM)
 
         self.response.json.return_value = updated_order.to_json()
@@ -832,12 +833,22 @@ def test_finalize_order_success(self):
         self.assertEqual(self.client.finalize_order(self.orderr, deadline), updated_orderr)
 
     def test_finalize_order_error(self):
-        updated_order = self.order.update(error=messages.Error.with_code('unauthorized'))
+        updated_order = self.order.update(
+            error=messages.Error.with_code('unauthorized'),
+            status=messages.STATUS_INVALID)
         self.response.json.return_value = updated_order.to_json()
 
         deadline = datetime.datetime(9999, 9, 9)
         self.assertRaises(errors.IssuanceError, self.client.finalize_order, self.orderr, deadline)
 
+    def test_finalize_order_invalid_status(self):
+        # https://github.com/certbot/certbot/issues/9296
+        order = self.order.update(error=None, status=messages.STATUS_INVALID)
+        self.response.json.return_value = order.to_json()
+        with self.assertRaises(errors.Error) as error:
+            self.client.finalize_order(self.orderr, datetime.datetime(9999, 9, 9))
+        self.assertIn("The certificate order failed", str(error.exception))
+
     def test_finalize_order_timeout(self):
         deadline = datetime.datetime.now() - datetime.timedelta(seconds=60)
         self.assertRaises(errors.TimeoutError, self.client.finalize_order, self.orderr, deadline)
@@ -845,6 +856,7 @@ def test_finalize_order_timeout(self):
     def test_finalize_order_alt_chains(self):
         updated_order = self.order.update(
             certificate='https://www.letsencrypt-demo.org/acme/cert/',
+            status=messages.STATUS_VALID
         )
         updated_orderr = self.orderr.update(body=updated_order,
                                             fullchain_pem=CERT_SAN_PEM,