Repository: certbot/certbot

Issue:
Check OCSP as part of determining if the certificate is due for renewal
There is [logic in `certbot/storage.py` to say that a certificate needs to renewed if it has been revoked](https://github.com/certbot/certbot/blob/d75908c645e0d13827b60c4c94491e0c41859ad4/certbot/storage.py#L936), but the [`storage.py` function that determines if a certificate revoked always returns `False`](https://github.com/certbot/certbot/blob/d75908c645e0d13827b60c4c94491e0c41859ad4/certbot/storage.py#L902). We have code in `certbot/ocsp.py` to obtain the OCSP status of the certificate and we should make use of it.

I think we should be careful here to only say that a certificate needs to be renewed if we can verify that the certificate is revoked. If we fail to obtain/validate the OCSP response for example, we should not say the certificate needs to be renewed. We probably also do not want to crash Certbot in this case.


Discussion:
Suggestion: at revocation time, ask a question about deleting the renewal conf.

We now have the ability to check OCSP in `certbot.ocsp`. This may be as simple as calling the functions there from `storage.py`, but we should make sure we're happy with the approach taken in `ocsp.py` and that it works well here.
related to #728
We've made a lot of changes to Certbot since this issue was opened. If you still have this issue with an up-to-date version of Certbot, can you please add a comment letting us know? This helps us to better see what issues are still affecting our users. If there is no further activity, this issue will be automatically closed.

You know, we don't actually ever get people complaining about this. I think we can close this.
I think point 2 of my original post is irrelevant and we won't have time to get to this for a while regardless, but I still personally think it'd be a good idea to check the OCSP status when determining whether certificates need to be renewed. Without it, I think the certificate could be revoked due to a problem CA side and Certbot won't reissue the certificate. Because of this, I'd personally like to reopen this issue and edit my original post or create a new issue to track this work.

Do you disagree @ohemorange?
I think it's a good idea in theory, but is not something that comes up in practice, so I don't see much need to prioritize it. If something changed and we did want to prioritize it, the issue is still here.
I worry that we could find ourselves in the future scrambling to implement this in response to mass revocation CA side and wishing we would have done it sooner. I personally think it's worth doing this in the next year or so especially since we already have the ability to make OCSP requests.
After talking about this out of band, we decided to reopen this. I updated my original comment to better describe what needs to be done here.
> I worry that we could find ourselves in the future scrambling to implement this in response to mass revocation CA side and wishing we would have done it sooner

The time has come. https://community.letsencrypt.org/t/revoking-certain-certificates-on-march-4/114864/15
Please consider adding this soon!
@bmw Any opinion about an ACME entry point to check if the certificate needs to be renewed? It could solve the problem of the OCSP cache and allow the renewals **before** the effective revocation.
There is the possibility that sidestepping the cache and hitting our origin directly will cause massive service degradation. The OCSP cache offload is ~90% - 95% of the traffic (depending on month) at our CDN which equates to hundreds of terabytes that don't hit our loadbalancers. There was an incident back in May of 2017 where the OCSP cache was purged and we had quite the painful incident.

Resolving patch:
diff --git a/certbot/certbot/_internal/storage.py b/certbot/certbot/_internal/storage.py
--- a/certbot/certbot/_internal/storage.py
+++ b/certbot/certbot/_internal/storage.py
@@ -15,6 +15,7 @@
 from certbot import crypto_util
 from certbot import errors
 from certbot import interfaces
+from certbot import ocsp
 from certbot import util
 from certbot._internal import cli
 from certbot._internal import constants
@@ -882,27 +883,33 @@ def names(self):
         with open(target) as f:
             return crypto_util.get_names_from_cert(f.read())
 
-    def ocsp_revoked(self, version=None):
-        # pylint: disable=unused-argument
+    def ocsp_revoked(self, version):
         """Is the specified cert version revoked according to OCSP?
 
-        Also returns True if the cert version is declared as intended
-        to be revoked according to Let's Encrypt OCSP extensions.
-        (If no version is specified, uses the current version.)
-
-        This method is not yet implemented and currently always returns
-        False.
+        Also returns True if the cert version is declared as revoked
+        according to OCSP. If OCSP status could not be determined, False
+        is returned.
 
         :param int version: the desired version number
 
-        :returns: whether the certificate is or will be revoked
+        :returns: True if the certificate is revoked, otherwise, False
         :rtype: bool
 
         """
-        # XXX: This query and its associated network service aren't
-        # implemented yet, so we currently return False (indicating that the
-        # certificate is not revoked).
-        return False
+        cert_path = self.version("cert", version)
+        chain_path = self.version("chain", version)
+        # While the RevocationChecker should return False if it failed to
+        # determine the OCSP status, let's ensure we don't crash Certbot by
+        # catching all exceptions here.
+        try:
+            return ocsp.RevocationChecker().ocsp_revoked_by_paths(cert_path,
+                                                                  chain_path)
+        except Exception as e:  # pylint: disable=broad-except
+            logger.warning(
+                "An error occurred determining the OCSP status of %s.",
+                cert_path)
+            logger.debug(str(e))
+            return False
 
     def autorenewal_is_enabled(self):
         """Is automatic renewal enabled for this cert?
diff --git a/certbot/certbot/ocsp.py b/certbot/certbot/ocsp.py
--- a/certbot/certbot/ocsp.py
+++ b/certbot/certbot/ocsp.py
@@ -68,8 +68,19 @@ def ocsp_revoked(self, cert):
         :rtype: bool
 
         """
-        cert_path, chain_path = cert.cert_path, cert.chain_path
+        return self.ocsp_revoked_by_paths(cert.cert_path, cert.chain_path)
 
+    def ocsp_revoked_by_paths(self, cert_path, chain_path):
+        # type: (str, str) -> bool
+        """Performs the OCSP revocation check
+
+        :param str cert_path: Certificate filepath
+        :param str chain_path: Certificate chain filepath
+
+        :returns: True if revoked; False if valid or the check failed or cert is expired.
+        :rtype: bool
+
+        """
         if self.broken:
             return False
 


Test patch:
diff --git a/certbot-ci/certbot_integration_tests/certbot_tests/test_main.py b/certbot-ci/certbot_integration_tests/certbot_tests/test_main.py
--- a/certbot-ci/certbot_integration_tests/certbot_tests/test_main.py
+++ b/certbot-ci/certbot_integration_tests/certbot_tests/test_main.py
@@ -595,6 +595,23 @@ def test_ocsp_status_live(context):
     assert output.count('REVOKED') == 1, 'Expected {0} to be REVOKED'.format(cert)
 
 
+def test_ocsp_renew(context):
+    """Test that revoked certificates are renewed."""
+    # Obtain a certificate
+    certname = context.get_domain('ocsp-renew')
+    context.certbot(['--domains', certname])
+
+    # Test that "certbot renew" does not renew the certificate
+    assert_cert_count_for_lineage(context.config_dir, certname, 1)
+    context.certbot(['renew'], force_renew=False)
+    assert_cert_count_for_lineage(context.config_dir, certname, 1)
+
+    # Revoke the certificate and test that it does renew the certificate
+    context.certbot(['revoke', '--cert-name', certname, '--no-delete-after-revoke'])
+    context.certbot(['renew'], force_renew=False)
+    assert_cert_count_for_lineage(context.config_dir, certname, 2)
+
+
 def test_dry_run_deactivate_authzs(context):
     """Test that Certbot deactivates authorizations when performing a dry run"""
 
diff --git a/certbot/tests/storage_test.py b/certbot/tests/storage_test.py
--- a/certbot/tests/storage_test.py
+++ b/certbot/tests/storage_test.py
@@ -672,10 +672,35 @@ def test_bad_kind(self):
             errors.CertStorageError,
             self.test_rc._update_link_to, "elephant", 17)
 
-    def test_ocsp_revoked(self):
-        # XXX: This is currently hardcoded to False due to a lack of an
-        #      OCSP server to test against.
-        self.assertFalse(self.test_rc.ocsp_revoked())
+    @mock.patch("certbot.ocsp.RevocationChecker.ocsp_revoked_by_paths")
+    def test_ocsp_revoked(self, mock_checker):
+        # Write out test files
+        for kind in ALL_FOUR:
+            self._write_out_kind(kind, 1)
+        version = self.test_rc.latest_common_version()
+        expected_cert_path = self.test_rc.version("cert", version)
+        expected_chain_path = self.test_rc.version("chain", version)
+
+        # Test with cert revoked
+        mock_checker.return_value = True
+        self.assertTrue(self.test_rc.ocsp_revoked(version))
+        self.assertEqual(mock_checker.call_args[0][0], expected_cert_path)
+        self.assertEqual(mock_checker.call_args[0][1], expected_chain_path)
+
+        # Test with cert not revoked
+        mock_checker.return_value = False
+        self.assertFalse(self.test_rc.ocsp_revoked(version))
+        self.assertEqual(mock_checker.call_args[0][0], expected_cert_path)
+        self.assertEqual(mock_checker.call_args[0][1], expected_chain_path)
+
+        # Test with error
+        mock_checker.side_effect = ValueError
+        with mock.patch("certbot._internal.storage.logger.warning") as logger:
+            self.assertFalse(self.test_rc.ocsp_revoked(version))
+        self.assertEqual(mock_checker.call_args[0][0], expected_cert_path)
+        self.assertEqual(mock_checker.call_args[0][1], expected_chain_path)
+        log_msg = logger.call_args[0][0]
+        self.assertIn("An error occurred determining the OCSP status", log_msg)
 
     def test_add_time_interval(self):
         from certbot._internal import storage