Repository: certbot/certbot

Issue:
Don't send OCSP requests for expired certificates
Let's Encrypt doesn't update OCSP for expired certificates. They are not required to do so. When someone runs `certbot certificates` with expired certificates and they're using our Python support for making OCSP requests rather than shelling out to `openssl`, they get output like:
```
Invalid OCSP response for /etc/letsencrypt/live/example.org/cert.pem: param nextUpdate is in the past…
```
There's not actually a problem here so we shouldn't be printing these warnings to users. 

The code that does this is at https://github.com/certbot/certbot/blob/d75908c645e0d13827b60c4c94491e0c41859ad4/certbot/cert_manager.py#L264-L267

Fixing that is simple enough, however, I think [one of our tests is relying on this behavior](https://github.com/certbot/certbot/blob/9a7f774706484dcec10cf5247d31bb547a5e1160/certbot-ci/certbot_integration_tests/certbot_tests/test_main.py#L545) (and if so, the test should be deleted) and we may also want to add code in `certbot.ocsp.ocsp_revoked` to check if the certificate is still valid before querying OCSP to help prevent this problem in the future.

This was reported at https://community.letsencrypt.org/t/invalid-ocsp-response-nextupdate-is-in-the-past/95640.

Discussion:
bmw: Looks like lint is angry and appveyor thought a more recent build of this PR started so it canceled this one.

If you continue to have issues with appveyor, let me know if you want me to take a look.

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 075da8376..f02e8fda3 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -10,7 +10,7 @@ Certbot adheres to [Semantic Versioning](https://semver.org/).
 
 ### Changed
 
-*
+* Don't send OCSP requests for expired certificates
 
 ### Fixed
 
diff --git a/certbot/cert_manager.py b/certbot/cert_manager.py
index 6d6d2e2e6..cd228ac12 100644
--- a/certbot/cert_manager.py
+++ b/certbot/cert_manager.py
@@ -262,7 +262,7 @@ def human_readable_cert_info(config, cert, skip_filter_checks=False):
         reasons.append('TEST_CERT')
     if cert.target_expiry <= now:
         reasons.append('EXPIRED')
-    if checker.ocsp_revoked(cert.cert, cert.chain):
+    elif checker.ocsp_revoked(cert):
         reasons.append('REVOKED')
 
     if reasons:
diff --git a/certbot/ocsp.py b/certbot/ocsp.py
index 0e35f023f..1cc1e7529 100644
--- a/certbot/ocsp.py
+++ b/certbot/ocsp.py
@@ -16,11 +16,13 @@ from cryptography.hazmat.backends import default_backend
 from cryptography.hazmat.primitives import serialization
 from cryptography.hazmat.primitives import hashes  # type: ignore
 from cryptography.exceptions import UnsupportedAlgorithm, InvalidSignature
+import pytz
 import requests
 
 from acme.magic_typing import Optional, Tuple  # pylint: disable=unused-import, no-name-in-module
 from certbot import crypto_util
 from certbot import errors
+from certbot.storage import RenewableCert # pylint: disable=unused-import
 from certbot import util
 
 logger = logging.getLogger(__name__)
@@ -48,21 +50,29 @@ class RevocationChecker(object):
             else:
                 self.host_args = lambda host: ["Host", host]
 
-    def ocsp_revoked(self, cert_path, chain_path):
-        # type: (str, str) -> bool
+    def ocsp_revoked(self, cert):
+        # type: (RenewableCert) -> bool
         """Get revoked status for a particular cert version.
 
         .. todo:: Make this a non-blocking call
 
-        :param str cert_path: Path to certificate
-        :param str chain_path: Path to intermediate cert
-        :returns: True if revoked; False if valid or the check failed
+        :param `.storage.RenewableCert` cert: Certificate object
+        :returns: True if revoked; False if valid or the check failed or cert is expired.
         :rtype: bool
 
         """
+        cert_path, chain_path = cert.cert, cert.chain
+
         if self.broken:
             return False
 
+        # Let's Encrypt doesn't update OCSP for expired certificates,
+        # so don't check OCSP if the cert is expired.
+        # https://github.com/certbot/certbot/issues/7152
+        now = pytz.UTC.fromutc(datetime.utcnow())
+        if cert.target_expiry <= now:
+            return False
+
         url, host = _determine_ocsp_server(cert_path)
         if not host or not url:
             return False
diff --git a/tools/dev_constraints.txt b/tools/dev_constraints.txt
index e2bec5e20..c23cf9cce 100644
--- a/tools/dev_constraints.txt
+++ b/tools/dev_constraints.txt
@@ -53,6 +53,9 @@ pyasn1==0.1.9
 pyasn1-modules==0.0.10
 Pygments==2.2.0
 pylint==1.9.4
+# If pynsist version is upgraded, our NSIS template windows-installer/template.nsi
+# must be upgraded if necessary using the new built-in one from pynsist.
+pynsist==2.4
 pytest==3.2.5
 pytest-cov==2.5.1
 pytest-forked==0.2
diff --git a/windows-installer/construct.py b/windows-installer/construct.py
index 15296d559..2427c0128 100644
--- a/windows-installer/construct.py
+++ b/windows-installer/construct.py
@@ -19,7 +19,7 @@ def main():
 
     installer_cfg_path = _generate_pynsist_config(repo_path, build_path)
 
-    _prepare_build_tools(venv_path, venv_python)
+    _prepare_build_tools(venv_path, venv_python, repo_path)
     _compile_wheels(repo_path, build_path, venv_python)
     _build_installer(installer_cfg_path, venv_path)
 
@@ -47,12 +47,12 @@ def _compile_wheels(repo_path, build_path, venv_python):
     subprocess.check_call(command)
 
 
-def _prepare_build_tools(venv_path, venv_python):
+def _prepare_build_tools(venv_path, venv_python, repo_path):
     print('Prepare build tools')
     subprocess.check_call([sys.executable, '-m', 'venv', venv_path])
     subprocess.check_call(['choco', 'upgrade', '-y', 'nsis'])
     subprocess.check_call([venv_python, '-m', 'pip', 'install', '--upgrade', 'pip'])
-    subprocess.check_call([venv_python, '-m', 'pip', 'install', 'wheel', 'pynsist'])
+    subprocess.check_call([venv_python, os.path.join(repo_path, 'tools', 'pip_install.py'), 'wheel', 'pynsist'])
 
 
 def _copy_assets(build_path, repo_path):
@@ -62,6 +62,9 @@ def _copy_assets(build_path, repo_path):
     os.makedirs(build_path)
     shutil.copy(os.path.join(repo_path, 'windows-installer', 'certbot.ico'), build_path)
     shutil.copy(os.path.join(repo_path, 'windows-installer', 'run.bat'), build_path)
+    shutil.copy(os.path.join(repo_path, 'windows-installer', 'template.nsi'), build_path)
+    shutil.copy(os.path.join(repo_path, 'windows-installer', 'renew-up.ps1'), build_path)
+    shutil.copy(os.path.join(repo_path, 'windows-installer', 'renew-down.ps1'), build_path)
 
 
 def _generate_pynsist_config(repo_path, build_path):
@@ -83,6 +86,7 @@ target=$INSTDIR\\run.bat
 
 [Build]
 directory=nsis
+nsi_template=template.nsi
 installer_name=certbot-{certbot_version}-installer-{installer_suffix}.exe
 
 [Python]
@@ -92,6 +96,8 @@ bitness={python_bitness}
 [Include]
 local_wheels=wheels\\*.whl
 files=run.bat
+      renew-up.ps1
+      renew-down.ps1
 
 [Command certbot]
 entry_point=certbot.main:main
diff --git a/windows-installer/renew-down.ps1 b/windows-installer/renew-down.ps1
new file mode 100644
index 000000000..60dc4d9e6
--- /dev/null
+++ b/windows-installer/renew-down.ps1
@@ -0,0 +1,6 @@
+$taskName = "Certbot Renew Task"
+
+$exists = Get-ScheduledTask | Where-Object {$_.TaskName -like $taskName}
+if ($exists) {
+    Unregister-ScheduledTask -TaskName $taskName -Confirm:$false
+}
diff --git a/windows-installer/renew-up.ps1 b/windows-installer/renew-up.ps1
new file mode 100644
index 000000000..c6a5fd9ea
--- /dev/null
+++ b/windows-installer/renew-up.ps1
@@ -0,0 +1,15 @@
+function Get-ScriptDirectory { Split-Path $MyInvocation.ScriptName }
+$down = Join-Path (Get-ScriptDirectory) 'renew-down.ps1'
+& $down
+
+$taskName = "Certbot Renew Task"
+
+$action = New-ScheduledTaskAction -Execute 'Powershell.exe' -Argument '-NoProfile -WindowStyle Hidden -Command "certbot renew"'
+$delay = New-TimeSpan -Hours 12
+$triggerAM = New-ScheduledTaskTrigger -Daily -At 12am -RandomDelay $delay
+$triggerPM = New-ScheduledTaskTrigger -Daily -At 12pm -RandomDelay $delay
+# NB: For now scheduled task is set up under SYSTEM account because Certbot Installer installs Certbot for all users.
+# If in the future we allow the Installer to install Certbot for one specific user, the scheduled task will need to
+# switch to this user, since Certbot will be available only for him.
+$principal = New-ScheduledTaskPrincipal -UserId SYSTEM -LogonType ServiceAccount -RunLevel Highest
+Register-ScheduledTask -Action $action -Trigger $triggerAM,$triggerPM -TaskName $taskName -Description "Execute twice a day the 'certbot renew' command, to renew managed certificates if needed." -Principal $principal
diff --git a/windows-installer/template.nsi b/windows-installer/template.nsi
new file mode 100644
index 000000000..0f366c22a
--- /dev/null
+++ b/windows-installer/template.nsi
@@ -0,0 +1,257 @@
+; This NSIS template is based on the built-in one in pynsist 2.3.
+; Added lines are enclosed within "CERTBOT CUSTOM BEGIN/END" comments.
+; If pynsist is upgraded, this template must be updated if necessary using the new built-in one.
+; Original file can be found here: https://github.com/takluyver/pynsist/blob/2.4/nsist/pyapp.nsi
+
+!define PRODUCT_NAME "[[ib.appname]]"
+!define PRODUCT_VERSION "[[ib.version]]"
+!define PY_VERSION "[[ib.py_version]]"
+!define PY_MAJOR_VERSION "[[ib.py_major_version]]"
+!define BITNESS "[[ib.py_bitness]]"
+!define ARCH_TAG "[[arch_tag]]"
+!define INSTALLER_NAME "[[ib.installer_name]]"
+!define PRODUCT_ICON "[[icon]]"
+
+; Marker file to tell the uninstaller that it's a user installation
+!define USER_INSTALL_MARKER _user_install_marker
+ 
+SetCompressor lzma
+
+; CERTBOT CUSTOM BEGIN
+; Administrator privileges are required to insert a new task in Windows Scheduler.
+; Also comment out some options to disable ability to choose AllUsers/CurrentUser install mode.
+; As a result, installer run always with admin privileges (because of MULTIUSER_EXECUTIONLEVEL),
+; using the AllUsers installation mode by default (because of MULTIUSER_INSTALLMODE_DEFAULT_CURRENTUSER
+; not set), and this default behavior cannot be overridden (because of MULTIUSER_MUI not set).
+; See https://nsis.sourceforge.io/Docs/MultiUser/Readme.html
+!define MULTIUSER_EXECUTIONLEVEL Admin
+;!define MULTIUSER_EXECUTIONLEVEL Highest
+;!define MULTIUSER_INSTALLMODE_DEFAULT_CURRENTUSER
+;!define MULTIUSER_MUI
+;!define MULTIUSER_INSTALLMODE_COMMANDLINE
+; CERTBOT CUSTOM END
+!define MULTIUSER_INSTALLMODE_INSTDIR "[[ib.appname]]"
+[% if ib.py_bitness == 64 %]
+!define MULTIUSER_INSTALLMODE_FUNCTION correct_prog_files
+[% endif %]
+!include MultiUser.nsh
+
+[% block modernui %]
+; Modern UI installer stuff 
+!include "MUI2.nsh"
+!define MUI_ABORTWARNING
+!define MUI_ICON "[[icon]]"
+!define MUI_UNICON "[[icon]]"
+
+; UI pages
+[% block ui_pages %]
+!insertmacro MUI_PAGE_WELCOME
+[% if license_file %]
+!insertmacro MUI_PAGE_LICENSE [[license_file]]
+[% endif %]
+; CERTBOT CUSTOM BEGIN
+; Disable the installation mode page (AllUsers/CurrentUser)
+;!insertmacro MULTIUSER_PAGE_INSTALLMODE
+; CERTBOT CUSTOM END
+!insertmacro MUI_PAGE_DIRECTORY
+!insertmacro MUI_PAGE_INSTFILES
+!insertmacro MUI_PAGE_FINISH
+[% endblock ui_pages %]
+!insertmacro MUI_LANGUAGE "English"
+[% endblock modernui %]
+
+Name "${PRODUCT_NAME} ${PRODUCT_VERSION}"
+OutFile "${INSTALLER_NAME}"
+ShowInstDetails show
+
+Section -SETTINGS
+  SetOutPath "$INSTDIR"
+  SetOverwrite ifnewer
+SectionEnd
+
+[% block sections %]
+
+Section "!${PRODUCT_NAME}" sec_app
+  SetRegView [[ib.py_bitness]]
+  SectionIn RO
+  File ${PRODUCT_ICON}
+  SetOutPath "$INSTDIR\pkgs"
+  File /r "pkgs\*.*"
+  SetOutPath "$INSTDIR"
+
+  ; Marker file for per-user install
+  StrCmp $MultiUser.InstallMode CurrentUser 0 +3
+    FileOpen $0 "$INSTDIR\${USER_INSTALL_MARKER}" w
+    FileClose $0
+    SetFileAttributes "$INSTDIR\${USER_INSTALL_MARKER}" HIDDEN
+
+  [% block install_files %]
+  ; Install files
+  [% for destination, group in grouped_files %]
+    SetOutPath "[[destination]]"
+    [% for file in group %]
+      File "[[ file ]]"
+    [% endfor %]
+  [% endfor %]
+  
+  ; Install directories
+  [% for dir, destination in ib.install_dirs %]
+    SetOutPath "[[ pjoin(destination, dir) ]]"
+    File /r "[[dir]]\*.*"
+  [% endfor %]
+  [% endblock install_files %]
+  
+  [% block install_shortcuts %]
+  ; Install shortcuts
+  ; The output path becomes the working directory for shortcuts
+  SetOutPath "%HOMEDRIVE%\%HOMEPATH%"
+  [% if single_shortcut %]
+    [% for scname, sc in ib.shortcuts.items() %]
+    CreateShortCut "$SMPROGRAMS\[[scname]].lnk" "[[sc['target'] ]]" \
+      '[[ sc['parameters'] ]]' "$INSTDIR\[[ sc['icon'] ]]"
+    [% endfor %]
+  [% else %]
+    [# Multiple shortcuts: create a directory for them #]
+    CreateDirectory "$SMPROGRAMS\${PRODUCT_NAME}"
+    [% for scname, sc in ib.shortcuts.items() %]
+    CreateShortCut "$SMPROGRAMS\${PRODUCT_NAME}\[[scname]].lnk" "[[sc['target'] ]]" \
+      '[[ sc['parameters'] ]]' "$INSTDIR\[[ sc['icon'] ]]"
+    [% endfor %]
+  [% endif %]
+  SetOutPath "$INSTDIR"
+  [% endblock install_shortcuts %]
+
+  [% block install_commands %]
+  [% if has_commands %]
+    DetailPrint "Setting up command-line launchers..."
+    nsExec::ExecToLog '[[ python ]] -Es "$INSTDIR\_assemble_launchers.py" [[ python ]] "$INSTDIR\bin"'
+
+    StrCmp $MultiUser.InstallMode CurrentUser 0 AddSysPathSystem
+      ; Add to PATH for current user
+      nsExec::ExecToLog '[[ python ]] -Es "$INSTDIR\_system_path.py" add_user "$INSTDIR\bin"'
+      GoTo AddedSysPath
+    AddSysPathSystem:
+      ; Add to PATH for all users
+      nsExec::ExecToLog '[[ python ]] -Es "$INSTDIR\_system_path.py" add "$INSTDIR\bin"'
+    AddedSysPath:
+  [% endif %]
+  [% endblock install_commands %]
+  
+  ; Byte-compile Python files.
+  DetailPrint "Byte-compiling Python modules..."
+  nsExec::ExecToLog '[[ python ]] -m compileall -q "$INSTDIR\pkgs"'
+  WriteUninstaller $INSTDIR\uninstall.exe
+  ; Add ourselves to Add/remove programs
+  WriteRegStr SHCTX "Software\Microsoft\Windows\CurrentVersion\Uninstall\${PRODUCT_NAME}" \
+                   "DisplayName" "${PRODUCT_NAME}"
+  WriteRegStr SHCTX "Software\Microsoft\Windows\CurrentVersion\Uninstall\${PRODUCT_NAME}" \
+                   "UninstallString" '"$INSTDIR\uninstall.exe"'
+  WriteRegStr SHCTX "Software\Microsoft\Windows\CurrentVersion\Uninstall\${PRODUCT_NAME}" \
+                   "InstallLocation" "$INSTDIR"
+  WriteRegStr SHCTX "Software\Microsoft\Windows\CurrentVersion\Uninstall\${PRODUCT_NAME}" \
+                   "DisplayIcon" "$INSTDIR\${PRODUCT_ICON}"
+  [% if ib.publisher is not none %]
+    WriteRegStr SHCTX "Software\Microsoft\Windows\CurrentVersion\Uninstall\${PRODUCT_NAME}" \
+                     "Publisher" "[[ib.publisher]]"
+  [% endif %]
+  WriteRegStr SHCTX "Software\Microsoft\Windows\CurrentVersion\Uninstall\${PRODUCT_NAME}" \
+                   "DisplayVersion" "${PRODUCT_VERSION}"
+  WriteRegDWORD SHCTX "Software\Microsoft\Windows\CurrentVersion\Uninstall\${PRODUCT_NAME}" \
+                   "NoModify" 1
+  WriteRegDWORD SHCTX "Software\Microsoft\Windows\CurrentVersion\Uninstall\${PRODUCT_NAME}" \
+                   "NoRepair" 1
+
+  ; CERTBOT CUSTOM BEGIN
+  ; Execute ps script to create the certbot renew task
+  DetailPrint "Setting up certbot renew scheduled task"
+  nsExec::ExecToStack 'powershell -inputformat none -ExecutionPolicy RemoteSigned -File "$INSTDIR\renew-up.ps1"'
+  ; CERTBOT CUSTOM END
+
+  ; Check if we need to reboot
+  IfRebootFlag 0 noreboot
+    MessageBox MB_YESNO "A reboot is required to finish the installation. Do you wish to reboot now?" \
+                /SD IDNO IDNO noreboot
+      Reboot
+  noreboot:
+SectionEnd
+
+Section "Uninstall"
+  ; CERTBOT CUSTOM BEGIN
+  ; Execute ps script to remove the certbot renew task
+  nsExec::ExecToStack 'powershell -inputformat none -ExecutionPolicy RemoteSigned -File "$INSTDIR\renew-down.ps1"'
+  ; CERTBOT CUSTOM END
+
+  SetRegView [[ib.py_bitness]]
+  SetShellVarContext all
+  IfFileExists "$INSTDIR\${USER_INSTALL_MARKER}" 0 +3
+    SetShellVarContext current
+    Delete "$INSTDIR\${USER_INSTALL_MARKER}"
+
+  Delete $INSTDIR\uninstall.exe
+  Delete "$INSTDIR\${PRODUCT_ICON}"
+  RMDir /r "$INSTDIR\pkgs"
+
+  ; Remove ourselves from %PATH%
+  [% block uninstall_commands %]
+  [% if has_commands %]
+    nsExec::ExecToLog '[[ python ]] -Es "$INSTDIR\_system_path.py" remove "$INSTDIR\bin"'
+  [% endif %]
+  [% endblock uninstall_commands %]
+
+  [% block uninstall_files %]
+  ; Uninstall files
+  [% for file, destination in ib.install_files %]
+    Delete "[[pjoin(destination, file)]]"
+  [% endfor %]
+  ; Uninstall directories
+  [% for dir, destination in ib.install_dirs %]
+    RMDir /r "[[pjoin(destination, dir)]]"
+  [% endfor %]
+  [% endblock uninstall_files %]
+
+  [% block uninstall_shortcuts %]
+  ; Uninstall shortcuts
+  [% if single_shortcut %]
+    [% for scname in ib.shortcuts %]
+      Delete "$SMPROGRAMS\[[scname]].lnk"
+    [% endfor %]
+  [% else %]
+    RMDir /r "$SMPROGRAMS\${PRODUCT_NAME}"
+  [% endif %]
+  [% endblock uninstall_shortcuts %]
+  RMDir $INSTDIR
+  DeleteRegKey SHCTX "Software\Microsoft\Windows\CurrentVersion\Uninstall\${PRODUCT_NAME}"
+SectionEnd
+
+[% endblock sections %]
+
+; Functions
+
+Function .onMouseOverSection
+    ; Find which section the mouse is over, and set the corresponding description.
+    FindWindow $R0 "#32770" "" $HWNDPARENT
+    GetDlgItem $R0 $R0 1043 ; description item (must be added to the UI)
+
+    [% block mouseover_messages %]
+    StrCmp $0 ${sec_app} "" +2
+      SendMessage $R0 ${WM_SETTEXT} 0 "STR:${PRODUCT_NAME}"
+    
+    [% endblock mouseover_messages %]
+FunctionEnd
+
+Function .onInit
+  !insertmacro MULTIUSER_INIT
+FunctionEnd
+
+Function un.onInit
+  !insertmacro MULTIUSER_UNINIT
+FunctionEnd
+
+[% if ib.py_bitness == 64 %]
+Function correct_prog_files
+  ; The multiuser machinery doesn't know about the different Program files
+  ; folder for 64-bit applications. Override the install dir it set.
+  StrCmp $MultiUser.InstallMode AllUsers 0 +2
+    StrCpy $INSTDIR "$PROGRAMFILES64\${MULTIUSER_INSTALLMODE_INSTDIR}"
+FunctionEnd
+[% endif %]
\ No newline at end of file


Test patch:
diff --git a/certbot/tests/ocsp_test.py b/certbot/tests/ocsp_test.py
index e8c1b9d03..680e2c2bb 100644
--- a/certbot/tests/ocsp_test.py
+++ b/certbot/tests/ocsp_test.py
@@ -16,6 +16,7 @@ try:
 except (ImportError, AttributeError):  # pragma: no cover
     ocsp_lib = None  # type: ignore
 import mock
+import pytz
 
 from certbot import errors
 from certbot.tests import util as test_util
@@ -72,21 +73,34 @@ class OCSPTestOpenSSL(unittest.TestCase):
     @mock.patch('certbot.ocsp._determine_ocsp_server')
     @mock.patch('certbot.util.run_script')
     def test_ocsp_revoked(self, mock_run, mock_determine):
+        now = pytz.UTC.fromutc(datetime.utcnow())
+        cert_obj = mock.MagicMock()
+        cert_obj.cert = "x"
+        cert_obj.chain = "y"
+        cert_obj.target_expiry = now + timedelta(hours=2)
+
         self.checker.broken = True
         mock_determine.return_value = ("", "")
-        self.assertEqual(self.checker.ocsp_revoked("x", "y"), False)
+        self.assertEqual(self.checker.ocsp_revoked(cert_obj), False)
 
         self.checker.broken = False
         mock_run.return_value = tuple(openssl_happy[1:])
-        self.assertEqual(self.checker.ocsp_revoked("x", "y"), False)
+        self.assertEqual(self.checker.ocsp_revoked(cert_obj), False)
         self.assertEqual(mock_run.call_count, 0)
 
         mock_determine.return_value = ("http://x.co", "x.co")
-        self.assertEqual(self.checker.ocsp_revoked("blah.pem", "chain.pem"), False)
+        self.assertEqual(self.checker.ocsp_revoked(cert_obj), False)
         mock_run.side_effect = errors.SubprocessError("Unable to load certificate launcher")
-        self.assertEqual(self.checker.ocsp_revoked("x", "y"), False)
+        self.assertEqual(self.checker.ocsp_revoked(cert_obj), False)
         self.assertEqual(mock_run.call_count, 2)
 
+        # cert expired
+        cert_obj.target_expiry = now
+        mock_determine.return_value = ("", "")
+        count_before = mock_determine.call_count
+        self.assertEqual(self.checker.ocsp_revoked(cert_obj), False)
+        self.assertEqual(mock_determine.call_count, count_before)
+
     def test_determine_ocsp_server(self):
         cert_path = test_util.vector_path('ocsp_certificate.pem')
 
@@ -131,18 +145,23 @@ class OSCPTestCryptography(unittest.TestCase):
         self.checker = ocsp.RevocationChecker()
         self.cert_path = test_util.vector_path('ocsp_certificate.pem')
         self.chain_path = test_util.vector_path('ocsp_issuer_certificate.pem')
+        self.cert_obj = mock.MagicMock()
+        self.cert_obj.cert = self.cert_path
+        self.cert_obj.chain = self.chain_path
+        now = pytz.UTC.fromutc(datetime.utcnow())
+        self.cert_obj.target_expiry = now + timedelta(hours=2)
 
     @mock.patch('certbot.ocsp._determine_ocsp_server')
     @mock.patch('certbot.ocsp._check_ocsp_cryptography')
     def test_ensure_cryptography_toggled(self, mock_revoke, mock_determine):
         mock_determine.return_value = ('http://example.com', 'example.com')
-        self.checker.ocsp_revoked(self.cert_path, self.chain_path)
+        self.checker.ocsp_revoked(self.cert_obj)
 
         mock_revoke.assert_called_once_with(self.cert_path, self.chain_path, 'http://example.com')
 
     def test_revoke(self):
         with _ocsp_mock(ocsp_lib.OCSPCertStatus.REVOKED, ocsp_lib.OCSPResponseStatus.SUCCESSFUL):
-            revoked = self.checker.ocsp_revoked(self.cert_path, self.chain_path)
+            revoked = self.checker.ocsp_revoked(self.cert_obj)
         self.assertTrue(revoked)
 
     def test_responder_is_issuer(self):
@@ -152,7 +171,7 @@ class OSCPTestCryptography(unittest.TestCase):
         with _ocsp_mock(ocsp_lib.OCSPCertStatus.REVOKED,
                         ocsp_lib.OCSPResponseStatus.SUCCESSFUL) as mocks:
             mocks['mock_response'].return_value.responder_name = issuer.subject
-            self.checker.ocsp_revoked(self.cert_path, self.chain_path)
+            self.checker.ocsp_revoked(self.cert_obj)
         # Here responder and issuer are the same. So only the signature of the OCSP
         # response is checked (using the issuer/responder public key).
         self.assertEqual(mocks['mock_check'].call_count, 1)
@@ -167,7 +186,7 @@ class OSCPTestCryptography(unittest.TestCase):
 
         with _ocsp_mock(ocsp_lib.OCSPCertStatus.REVOKED,
                         ocsp_lib.OCSPResponseStatus.SUCCESSFUL) as mocks:
-            self.checker.ocsp_revoked(self.cert_path, self.chain_path)
+            self.checker.ocsp_revoked(self.cert_obj)
         # Here responder and issuer are not the same. Two signatures will be checked then,
         # first to verify the responder cert (using the issuer public key), second to
         # to verify the OCSP response itself (using the responder public key).
@@ -181,17 +200,17 @@ class OSCPTestCryptography(unittest.TestCase):
         # Server return an invalid HTTP response
         with _ocsp_mock(ocsp_lib.OCSPCertStatus.UNKNOWN, ocsp_lib.OCSPResponseStatus.SUCCESSFUL,
                         http_status_code=400):
-            revoked = self.checker.ocsp_revoked(self.cert_path, self.chain_path)
+            revoked = self.checker.ocsp_revoked(self.cert_obj)
         self.assertFalse(revoked)
 
         # OCSP response in invalid
         with _ocsp_mock(ocsp_lib.OCSPCertStatus.UNKNOWN, ocsp_lib.OCSPResponseStatus.UNAUTHORIZED):
-            revoked = self.checker.ocsp_revoked(self.cert_path, self.chain_path)
+            revoked = self.checker.ocsp_revoked(self.cert_obj)
         self.assertFalse(revoked)
 
         # OCSP response is valid, but certificate status is unknown
         with _ocsp_mock(ocsp_lib.OCSPCertStatus.UNKNOWN, ocsp_lib.OCSPResponseStatus.SUCCESSFUL):
-            revoked = self.checker.ocsp_revoked(self.cert_path, self.chain_path)
+            revoked = self.checker.ocsp_revoked(self.cert_obj)
         self.assertFalse(revoked)
 
         # The OCSP response says that the certificate is revoked, but certificate
@@ -200,32 +219,32 @@ class OSCPTestCryptography(unittest.TestCase):
             with mock.patch('cryptography.x509.Extensions.get_extension_for_class',
                             side_effect=x509.ExtensionNotFound(
                                 'Not found', x509.AuthorityInformationAccessOID.OCSP)):
-                revoked = self.checker.ocsp_revoked(self.cert_path, self.chain_path)
+                revoked = self.checker.ocsp_revoked(self.cert_obj)
         self.assertFalse(revoked)
 
         # OCSP response uses an unsupported signature.
         with _ocsp_mock(ocsp_lib.OCSPCertStatus.REVOKED, ocsp_lib.OCSPResponseStatus.SUCCESSFUL,
                         check_signature_side_effect=UnsupportedAlgorithm('foo')):
-            revoked = self.checker.ocsp_revoked(self.cert_path, self.chain_path)
+            revoked = self.checker.ocsp_revoked(self.cert_obj)
         self.assertFalse(revoked)
 
         # OSCP signature response is invalid.
         with _ocsp_mock(ocsp_lib.OCSPCertStatus.REVOKED, ocsp_lib.OCSPResponseStatus.SUCCESSFUL,
                         check_signature_side_effect=InvalidSignature('foo')):
-            revoked = self.checker.ocsp_revoked(self.cert_path, self.chain_path)
+            revoked = self.checker.ocsp_revoked(self.cert_obj)
         self.assertFalse(revoked)
 
         # Assertion error on OCSP response validity
         with _ocsp_mock(ocsp_lib.OCSPCertStatus.REVOKED, ocsp_lib.OCSPResponseStatus.SUCCESSFUL,
                         check_signature_side_effect=AssertionError('foo')):
-            revoked = self.checker.ocsp_revoked(self.cert_path, self.chain_path)
+            revoked = self.checker.ocsp_revoked(self.cert_obj)
         self.assertFalse(revoked)
 
         # No responder cert in OCSP response
         with _ocsp_mock(ocsp_lib.OCSPCertStatus.REVOKED,
                         ocsp_lib.OCSPResponseStatus.SUCCESSFUL) as mocks:
             mocks['mock_response'].return_value.certificates = []
-            revoked = self.checker.ocsp_revoked(self.cert_path, self.chain_path)
+            revoked = self.checker.ocsp_revoked(self.cert_obj)
         self.assertFalse(revoked)
 
         # Responder cert is not signed by certificate issuer
@@ -234,7 +253,7 @@ class OSCPTestCryptography(unittest.TestCase):
             cert = mocks['mock_response'].return_value.certificates[0]
             mocks['mock_response'].return_value.certificates[0] = mock.Mock(
                 issuer='fake', subject=cert.subject)
-            revoked = self.checker.ocsp_revoked(self.cert_path, self.chain_path)
+            revoked = self.checker.ocsp_revoked(self.cert_obj)
         self.assertFalse(revoked)
 
         with _ocsp_mock(ocsp_lib.OCSPCertStatus.REVOKED, ocsp_lib.OCSPResponseStatus.SUCCESSFUL):
@@ -245,7 +264,7 @@ class OSCPTestCryptography(unittest.TestCase):
                 with mock.patch('cryptography.x509.Extensions.get_extension_for_class',
                                 side_effect=x509.ExtensionNotFound(
                                     'Not found', x509.AuthorityInformationAccessOID.OCSP)):
-                    revoked = self.checker.ocsp_revoked(self.cert_path, self.chain_path)
+                    revoked = self.checker.ocsp_revoked(self.cert_obj)
         self.assertFalse(revoked)