diff --git a/certomancer/registry/pki_arch.py b/certomancer/registry/pki_arch.py index 79a82f6..31ab095 100644 --- a/certomancer/registry/pki_arch.py +++ b/certomancer/registry/pki_arch.py @@ -1156,6 +1156,7 @@ def summon_responder( issuer_cert_label=issuer_cert_label, is_aa_responder=info.is_aa_responder, ), + validity=info.validity_period, response_extensions=extra_extensions, ) diff --git a/certomancer/registry/svc_config/ocsp.py b/certomancer/registry/svc_config/ocsp.py index c7bb7c4..b18df85 100644 --- a/certomancer/registry/svc_config/ocsp.py +++ b/certomancer/registry/svc_config/ocsp.py @@ -1,5 +1,5 @@ from dataclasses import dataclass, field -from datetime import datetime +from datetime import datetime, timedelta from typing import TYPE_CHECKING, List, Optional, Tuple from asn1crypto import ocsp, x509 @@ -76,6 +76,12 @@ class OCSPResponderServiceInfo(ServiceInfo): or regular certificates. """ + validity_period: Optional[timedelta] = timedelta(minutes=10) + """ + Validity period for the OCSP response (i.e. time until ``nextUpdate``). + Defaults to ten minutes. ``None`` implies no ``nextUpdate``. + """ + @classmethod def process_entries(cls, config_dict): try: @@ -83,6 +89,15 @@ def process_entries(cls, config_dict): except KeyError: pass + try: + period_kwargs = config_dict['validity_period'] + if not period_kwargs: + config_dict['validity_period'] = None + else: + config_dict['validity_period'] = timedelta(**period_kwargs) + except KeyError: + pass + parse_extension_settings(config_dict, 'ocsp_extensions') def resolve_issuer_cert(self, arch: 'PKIArchitecture') -> CertLabel: diff --git a/certomancer/services.py b/certomancer/services.py index 40fffa5..4335ecb 100644 --- a/certomancer/services.py +++ b/certomancer/services.py @@ -354,7 +354,7 @@ def __init__( signature_algo: algos.SignedDigestAlgorithm, at_time: datetime, revinfo_interface: RevocationInfoInterface, - validity: timedelta = timedelta(minutes=10), + validity: Optional[timedelta] = timedelta(minutes=10), response_extensions: Optional[List[ocsp.ResponseDataExtension]] = None, ): self.responder_cert = responder_cert @@ -380,16 +380,15 @@ def format_single_ocsp_response( cid, self.at_time ) - single_resp = ocsp.SingleResponse( - { - 'cert_id': cid, - 'cert_status': cert_status, - 'this_update': self.at_time, - 'next_update': self.at_time + self.validity, - 'single_extensions': exts or None, - } - ) - return single_resp + response_data = { + 'cert_id': cid, + 'cert_status': cert_status, + 'this_update': self.at_time, + 'single_extensions': exts or None, + } + if self.validity is not None: + response_data['next_update'] = self.at_time + self.validity + return ocsp.SingleResponse(response_data) def build_ocsp_response(self, req: ocsp.OCSPRequest) -> ocsp.OCSPResponse: nonce_asn1 = req.nonce_value diff --git a/tests/data/full-service-config.yml b/tests/data/full-service-config.yml index 528fcb7..07e30d4 100644 --- a/tests/data/full-service-config.yml +++ b/tests/data/full-service-config.yml @@ -163,6 +163,11 @@ services: for-issuer: interm responder-cert: interm-ocsp signing-key: interm-ocsp + interm2: + for-issuer: interm + responder-cert: interm-ocsp + signing-key: interm-ocsp + validity-period: {} crl-repo: root: for-issuer: root diff --git a/tests/data/with-services.yml b/tests/data/with-services.yml index 8dfec23..dda5588 100644 --- a/tests/data/with-services.yml +++ b/tests/data/with-services.yml @@ -95,3 +95,4 @@ pki-architectures: responder-cert: role-aa signing-key: aa is-aa-responder: true + validity-period: {minutes: 2} diff --git a/tests/test_services.py b/tests/test_services.py index 63707d9..1a3e79a 100644 --- a/tests/test_services.py +++ b/tests/test_services.py @@ -2,7 +2,7 @@ import importlib import itertools from collections import namedtuple -from datetime import datetime +from datetime import datetime, timedelta, timezone import pytest import pytz @@ -210,6 +210,27 @@ def test_ocsp(requests_mock, time, expected): assert status == expected +@pytest.mark.parametrize( + "time, expected", [('2020-11-05', 'good'), ('2020-12-05', 'revoked')] +) +def test_ocsp_without_nextupdate(requests_mock, time, expected): + setup = RSA_SETUP + setup.illusionist.register(requests_mock) + with open('tests/data/signer2-ocsp-req.der', 'rb') as req_in: + req_data = req_in.read() + with freeze_time(time): + response = requests.post( + "http://test.test/testing-ca/ocsp/interm2", data=req_data + ) + resp: ocsp.OCSPResponse = ocsp.OCSPResponse.load(response.content) + assert resp['response_status'].native == 'successful' + + rdata = resp['response_bytes']['response'].parsed['tbs_response_data'] + status = rdata['responses'][0]['cert_status'].name + assert rdata['responses'][0]['next_update'].native is None + assert status == expected + + @pytest.mark.parametrize( "time, expected", [('2020-11-05', 'good'), ('2020-12-05', 'revoked')] ) @@ -236,6 +257,9 @@ def test_aa_ocsp(requests_mock, time, expected): rdata = resp['response_bytes']['response'].parsed['tbs_response_data'] status = rdata['responses'][0]['cert_status'].name assert status == expected + this_update = rdata['responses'][0]['this_update'].native + next_update = rdata['responses'][0]['next_update'].native + assert next_update == this_update + timedelta(minutes=2) @freeze_time('2020-11-01')