Skip to content

Commit 8d508dc

Browse files
Improve CRL cache usage
... by not downloading new CRLs until the cached ones have been shown to be inadequate.
1 parent e6a3acc commit 8d508dc

File tree

3 files changed

+253
-33
lines changed

3 files changed

+253
-33
lines changed

pyhanko_certvalidator/revinfo/manager.py

+47-6
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,19 @@
11
from datetime import datetime
2-
from typing import Dict, Iterable, List, Optional, Set
2+
from typing import (
3+
AsyncGenerator,
4+
AsyncIterable,
5+
AsyncIterator,
6+
Dict,
7+
Iterable,
8+
List,
9+
Optional,
10+
Set,
11+
)
312

413
from asn1crypto import crl, ocsp, x509
514

615
from pyhanko_certvalidator.authority import Authority
7-
from pyhanko_certvalidator.errors import OCSPFetchError
16+
from pyhanko_certvalidator.errors import CRLFetchError, OCSPFetchError
817
from pyhanko_certvalidator.fetchers import Fetchers
918
from pyhanko_certvalidator.ltv.poe import (
1019
KnownPOE,
@@ -189,9 +198,26 @@ def check_crl_issuer(self, certificate_list) -> Optional[x509.Certificate]:
189198

190199
return self._crl_issuer_map.get(certificate_list.signature)
191200

192-
async def async_retrieve_crls(self, cert) -> List[CRLContainer]:
201+
def currently_available_crls(self) -> List[CRLContainer]:
193202
"""
194-
.. versionadded:: 0.20.0
203+
.. versionadded:: 0.27.0
204+
205+
Return all currently available CRLs.
206+
207+
:return:
208+
A list of :class:`CRLContainer` objects
209+
"""
210+
result = list(self._crls)
211+
if self._fetchers:
212+
crls = self._fetchers.crl_fetcher.fetched_crls()
213+
result.extend(CRLContainer(crl_data) for crl_data in crls)
214+
return result
215+
216+
async def fetch_crls(self, cert) -> List[CRLContainer]:
217+
"""
218+
.. versionadded:: 0.27.0
219+
220+
Download all relevant CRLs for a given certificate.
195221
196222
:param cert:
197223
An asn1crypto.x509.Certificate object
@@ -200,15 +226,30 @@ async def async_retrieve_crls(self, cert) -> List[CRLContainer]:
200226
A list of :class:`CRLContainer` objects
201227
"""
202228
if not self._fetchers:
203-
return self._crls
229+
raise CRLFetchError("No CRL fetcher available")
204230

205231
fetchers = self._fetchers
206232
try:
207233
crls = fetchers.crl_fetcher.fetched_crls_for_cert(cert)
208234
except KeyError:
209235
crls = await fetchers.crl_fetcher.fetch(cert)
210236
conts = [CRLContainer(crl_data) for crl_data in crls]
211-
return conts + self._crls
237+
return conts
238+
239+
async def async_retrieve_crls(self, cert) -> List[CRLContainer]:
240+
"""
241+
.. versionadded:: 0.20.0
242+
243+
:param cert:
244+
An asn1crypto.x509.Certificate object
245+
246+
:return:
247+
A list of :class:`CRLContainer` objects
248+
"""
249+
crls = self.currently_available_crls()
250+
if self._fetchers:
251+
crls.extend(await self.fetch_crls(cert))
252+
return crls
212253

213254
async def async_retrieve_ocsps(
214255
self, cert, authority: Authority

pyhanko_certvalidator/revinfo/validate_crl.py

+84-23
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
PSSParameterMismatch,
2222
RevokedError,
2323
)
24+
from pyhanko_certvalidator.ltv.poe import POEManager
2425
from pyhanko_certvalidator.ltv.types import ValidationTimingParams
2526
from pyhanko_certvalidator.path import ValidationPath
2627
from pyhanko_certvalidator.policy_decl import CertRevTrustPolicy
@@ -729,7 +730,11 @@ def _maybe_get_delta_crl(
729730
parent_crl_aki=certificate_list.authority_key_identifier,
730731
)
731732
if not delta_certificate_list_cont:
732-
return None
733+
raise CRLValidationIndeterminateError(
734+
"Delta CRL matching Freshest CRL extension not available",
735+
failures=[],
736+
suspect_stale=None,
737+
)
733738

734739
delta_certificate_list = delta_certificate_list_cont.crl_data
735740

@@ -924,17 +929,14 @@ def _check_cert_on_crl_and_delta(
924929

925930

926931
async def _classify_relevant_crls(
927-
revinfo_manager: RevinfoManager,
928-
cert: x509.Certificate,
932+
certificate_lists: List[CRLContainer],
933+
poe_manager: POEManager,
929934
errs: _CRLErrs,
930935
control_time: Optional[datetime] = None,
931936
):
932937
# NOTE: the control_time parameter is only used in the time sliding
933938
# algorithm code path for AdES validation
934939

935-
certificate_lists = await revinfo_manager.async_retrieve_crls(cert)
936-
poe_manager = revinfo_manager.poe_manager
937-
938940
complete_lists_by_issuer = defaultdict(list)
939941
delta_lists_by_issuer = defaultdict(list)
940942
for certificate_list_cont in certificate_lists:
@@ -1046,11 +1048,6 @@ async def verify_crl(
10461048

10471049
revinfo_manager = validation_context.revinfo_manager
10481050
errs = _CRLErrs()
1049-
(
1050-
complete_lists_by_issuer,
1051-
delta_lists_by_issuer,
1052-
) = await _classify_relevant_crls(revinfo_manager, cert, errs)
1053-
10541051
try:
10551052
cert_issuer_auth = path.find_issuing_authority(cert)
10561053
except LookupError:
@@ -1059,6 +1056,14 @@ async def verify_crl(
10591056
f"{proc_state.describe_cert()} in path."
10601057
)
10611058

1059+
# First, make an attempt to validate without downloading any extra CRLs
1060+
certificate_lists = revinfo_manager.currently_available_crls()
1061+
poe_manager = revinfo_manager.poe_manager
1062+
(
1063+
complete_lists_by_issuer,
1064+
delta_lists_by_issuer,
1065+
) = await _classify_relevant_crls(certificate_lists, poe_manager, errs)
1066+
10621067
# In the main loop, only complete CRLs are processed, so delta CRLs are
10631068
# weeded out of the to-do list
10641069
crls_to_process = []
@@ -1068,27 +1073,76 @@ async def verify_crl(
10681073

10691074
checked_reasons = set()
10701075

1071-
for certificate_list_cont in crls_to_process:
1076+
async def _process(crl_container, deltas):
1077+
nonlocal checked_reasons
1078+
nonlocal errs
10721079
try:
10731080
interim_reasons = await _handle_single_crl(
10741081
cert=cert,
10751082
cert_issuer_auth=cert_issuer_auth,
1076-
certificate_list_cont=certificate_list_cont,
1083+
certificate_list_cont=crl_container,
10771084
path=path,
10781085
validation_context=validation_context,
1079-
delta_lists_by_issuer=delta_lists_by_issuer,
1086+
delta_lists_by_issuer=deltas,
10801087
use_deltas=use_deltas,
10811088
errs=errs,
10821089
proc_state=proc_state,
10831090
)
10841091
if interim_reasons is not None:
10851092
# Step l
10861093
checked_reasons |= interim_reasons
1094+
except CRLValidationIndeterminateError as e:
1095+
errs.append(e.msg, certificate_list_cont)
10871096
except ValueError as e:
10881097
msg = "Generic processing error while validating CRL."
10891098
logging.debug(msg, exc_info=e)
10901099
errs.append(msg, certificate_list_cont)
10911100

1101+
for certificate_list_cont in crls_to_process:
1102+
await _process(certificate_list_cont, delta_lists_by_issuer)
1103+
1104+
exc = _process_crl_completeness(
1105+
checked_reasons, total_crls, errs, proc_state
1106+
)
1107+
if exc is None:
1108+
return
1109+
elif not revinfo_manager.fetching_allowed:
1110+
raise exc
1111+
1112+
# If we're not done checking CRLs, but we are allowed to fetch more,
1113+
# let's go download some more CRLs...
1114+
1115+
# TODO scan Freshest CRL extensions for delta CRLs?
1116+
1117+
extra_certificate_lists = await revinfo_manager.fetch_crls(cert)
1118+
(
1119+
extra_complete_lists_by_issuer,
1120+
extra_delta_lists_by_issuer,
1121+
) = await _classify_relevant_crls(
1122+
extra_certificate_lists, poe_manager, errs
1123+
)
1124+
1125+
combined_deltas = {
1126+
k: delta_lists_by_issuer.get(k, [])
1127+
+ extra_delta_lists_by_issuer.get(k, [])
1128+
for k in set(delta_lists_by_issuer.keys()).union(
1129+
extra_delta_lists_by_issuer.keys()
1130+
)
1131+
}
1132+
1133+
crls_to_process = []
1134+
for issuer, issuer_crls in complete_lists_by_issuer.items():
1135+
# some of the new deltas might complement CRLs that we already had
1136+
if issuer in extra_delta_lists_by_issuer:
1137+
crls_to_process.extend(issuer_crls)
1138+
for issuer_crls in extra_complete_lists_by_issuer.values():
1139+
crls_to_process.extend(issuer_crls)
1140+
1141+
for certificate_list_cont in crls_to_process:
1142+
await _process(certificate_list_cont, combined_deltas)
1143+
1144+
total_crls += len(crls_to_process)
1145+
10921146
exc = _process_crl_completeness(
10931147
checked_reasons, total_crls, errs, proc_state
10941148
)
@@ -1217,15 +1271,18 @@ async def _assess_crl_relevance(
12171271
if interim_reasons is None:
12181272
continue
12191273

1274+
delta = None
12201275
if use_deltas:
1221-
delta = _maybe_get_delta_crl(
1222-
certificate_list=certificate_list,
1223-
crl_issuer=putative_issuer,
1224-
delta_lists_by_issuer=delta_lists_by_issuer,
1225-
errs=errs,
1226-
)
1227-
else:
1228-
delta = None
1276+
try:
1277+
delta = _maybe_get_delta_crl(
1278+
certificate_list=certificate_list,
1279+
crl_issuer=putative_issuer,
1280+
delta_lists_by_issuer=delta_lists_by_issuer,
1281+
errs=errs,
1282+
)
1283+
except CRLValidationIndeterminateError as e:
1284+
errs.append(e.msg, certificate_list)
1285+
continue
12291286

12301287
prov = ProvisionalCRLTrust(path=cand_path, delta=delta)
12311288
provisional_results.append(prov)
@@ -1271,8 +1328,12 @@ async def collect_relevant_crls_with_paths(
12711328

12721329
proc_state = proc_state or ValProcState(cert_path_stack=ConsList.sing(path))
12731330
errs = _CRLErrs()
1331+
candidate_crls = revinfo_manager.currently_available_crls()
12741332
classify_job = _classify_relevant_crls(
1275-
revinfo_manager, cert, errs, control_time=control_time
1333+
candidate_crls,
1334+
revinfo_manager.poe_manager,
1335+
errs,
1336+
control_time=control_time,
12761337
)
12771338
complete_lists_by_issuer, delta_lists_by_issuer = await classify_job
12781339

0 commit comments

Comments
 (0)