Skip to content

Commit f23f31a

Browse files
Additionally test PKITS with CRL fetchers
1 parent 8d508dc commit f23f31a

File tree

3 files changed

+135
-8
lines changed

3 files changed

+135
-8
lines changed

pyhanko_certvalidator/revinfo/validate_crl.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,7 @@ async def _find_candidate_crl_issuer_certs(
8383
)
8484
if not candidates and cert_registry.fetcher is not None:
8585
candidates = []
86-
valid_names = {crl_authority_name, delegated_issuer}
86+
valid_names = (crl_authority_name, delegated_issuer)
8787
# Try to download certificates from URLs in the AIA extension,
8888
# if there is one
8989
async for cert in cert_registry.fetcher.fetch_crl_issuers(

pyhanko_certvalidator/validate.py

+19-6
Original file line numberDiff line numberDiff line change
@@ -1338,11 +1338,16 @@ async def _check_revocation(
13381338
err_str = '; '.join(str(f) for f in failures)
13391339
else:
13401340
err_str = 'an applicable OCSP response could not be found'
1341-
raise InsufficientRevinfoError.from_state(
1341+
msg = (
13421342
f"The path could not be validated because the mandatory OCSP "
1343-
f"check(s) for {proc_state.describe_cert()} failed: {err_str}",
1344-
proc_state,
1343+
f"check(s) for {proc_state.describe_cert()} failed: {err_str}"
13451344
)
1345+
if ocsp_suspect_stale_since:
1346+
raise StaleRevinfoError.format(
1347+
msg, ocsp_suspect_stale_since, proc_state
1348+
)
1349+
else:
1350+
raise InsufficientRevinfoError.from_state(msg, proc_state)
13461351
status_good = (
13471352
ocsp_status_good
13481353
and rev_rule != RevocationCheckingRule.CRL_AND_OCSP_REQUIRED
@@ -1385,11 +1390,19 @@ async def _check_revocation(
13851390
err_str = '; '.join(str(f) for f in failures)
13861391
else:
13871392
err_str = 'an applicable CRL could not be found'
1388-
raise InsufficientRevinfoError.from_state(
1393+
msg = (
13891394
f"The path could not be validated because the mandatory CRL "
1390-
f"check(s) for {proc_state.describe_cert()} failed: {err_str}",
1391-
proc_state,
1395+
f"check(s) for {proc_state.describe_cert()} failed: {err_str}"
13921396
)
1397+
if crl_suspect_stale_since:
1398+
raise StaleRevinfoError.format(
1399+
msg, crl_suspect_stale_since, proc_state
1400+
)
1401+
else:
1402+
raise InsufficientRevinfoError.from_state(
1403+
msg,
1404+
proc_state,
1405+
)
13931406

13941407
# If we still didn't find a match, the certificate has CRL/OCSP entries
13951408
# but we couldn't query any of them. Let's check if this is disqualifying.

tests/test_validate.py

+115-1
Original file line numberDiff line numberDiff line change
@@ -36,8 +36,11 @@
3636
from pyhanko_certvalidator.ltv.poe import POEManager
3737
from pyhanko_certvalidator.path import QualifiedPolicy, ValidationPath
3838
from pyhanko_certvalidator.policy_decl import (
39+
CertRevTrustPolicy,
3940
DisallowWeakAlgorithmsPolicy,
4041
NonRevokedStatusAssertion,
42+
RevocationCheckingPolicy,
43+
RevocationCheckingRule,
4144
)
4245
from pyhanko_certvalidator.registry import (
4346
CertificateRegistry,
@@ -680,6 +683,77 @@ def test_nist_pkits(test_case: PKITSTestCase):
680683
)
681684

682685

686+
def nist_revocation_tests():
687+
specs = read_pkits_test_params()
688+
return [spec for spec in specs if spec.check_revocation]
689+
690+
691+
class ReturnPredeterminedCRLs(CRLFetcher):
692+
def __init__(self, crls):
693+
self.crls = crls
694+
695+
def fetched_crls_for_cert(
696+
self, cert: x509.Certificate
697+
) -> Iterable[crl.CertificateList]:
698+
raise KeyError()
699+
700+
def fetched_crls(self) -> Iterable[crl.CertificateList]:
701+
return ()
702+
703+
async def fetch(self, cert: x509.Certificate, *, use_deltas=None):
704+
return self.crls
705+
706+
707+
@freeze_time('2022-05-01')
708+
@pytest.mark.parametrize(
709+
'test_case', nist_revocation_tests(), ids=lambda case: str(case.test_info)
710+
)
711+
def test_nist_pkits_with_simulated_crl_downloads(test_case: PKITSTestCase):
712+
713+
fetchers = Fetchers(
714+
ocsp_fetcher=MockOCSPFetcher(),
715+
crl_fetcher=ReturnPredeterminedCRLs(test_case.crls),
716+
cert_fetcher=MockCertFetcher(),
717+
)
718+
719+
# TODO rework failure messages and realign fixtures
720+
# so we can do message validations here.
721+
# Also consider having multiple variant runs with
722+
# slightly different revo policies
723+
policy = RevocationCheckingPolicy(
724+
RevocationCheckingRule.CRL_REQUIRED,
725+
RevocationCheckingRule.CRL_REQUIRED,
726+
)
727+
context = ValidationContext(
728+
trust_roots=test_case.roots,
729+
other_certs=test_case.other_certs,
730+
allow_fetching=True,
731+
fetchers=fetchers,
732+
revinfo_policy=CertRevTrustPolicy(
733+
revocation_checking_policy=policy,
734+
),
735+
# adjust default algo policy to pass NIST tests
736+
algorithm_usage_policy=DisallowWeakAlgorithmsPolicy(
737+
weak_hash_algos={'md2', 'md5'}, dsa_key_size_threshold=1024
738+
),
739+
)
740+
741+
if test_case.path is None:
742+
paths = context.path_builder.build_paths(test_case.cert)
743+
assert 1 == len(paths)
744+
path: ValidationPath = paths[0]
745+
else:
746+
path = test_case.path
747+
748+
err = test_case.expected_error
749+
params = test_case.pkix_params
750+
if err is not None:
751+
with pytest.raises(err.err_class):
752+
validate_path(context, path, parameters=params)
753+
else:
754+
validate_path(context, path, parameters=params)
755+
756+
683757
@dataclass(frozen=True)
684758
class PKITSUserNoticeTestCase:
685759
test_info: CannedTestInfo
@@ -874,6 +948,13 @@ def test_do_not_fetch_crl_if_cache_sufficient():
874948
]
875949
moment = datetime(2020, 10, 2, tzinfo=timezone.utc)
876950
fetchers = MockFetcherBackend().get_fetchers()
951+
952+
crl_fetcher = MockCRLFetcher()
953+
fetchers = Fetchers(
954+
ocsp_fetcher=MockOCSPFetcher(),
955+
crl_fetcher=crl_fetcher,
956+
cert_fetcher=MockCertFetcher(),
957+
)
877958
context = ValidationContext(
878959
trust_roots=ca_certs,
879960
other_certs=other_certs,
@@ -884,7 +965,7 @@ def test_do_not_fetch_crl_if_cache_sufficient():
884965
revocation_mode='require',
885966
)
886967

887-
assert fetchers.crl_fetcher.calls == 0
968+
assert crl_fetcher.calls == 0
888969

889970
paths = context.path_builder.build_paths(cert)
890971
assert 1 == len(paths)
@@ -963,3 +1044,36 @@ def test_fail_validation_if_required_delta_crl_not_available():
9631044
path: ValidationPath = paths[0]
9641045
with pytest.raises(InsufficientRevinfoError, match=".*Delta CRL.*"):
9651046
validate_path(context, path)
1047+
1048+
1049+
def test_context_retrieve_all_crls():
1050+
1051+
cert = load_nist_cert('InvaliddeltaCRLTest4EE.crt')
1052+
ca_certs = [load_nist_cert('TrustAnchorRootCertificate.crt')]
1053+
other_certs = [load_nist_cert('deltaCRLCA1Cert.crt')]
1054+
crl1 = load_nist_crl('deltaCRLCA1CRL.crl')
1055+
crl2 = load_nist_crl('TrustAnchorRootCRL.crl')
1056+
crl3 = load_nist_crl('deltaCRLCA1deltaCRL.crl')
1057+
crls = [crl1, crl2]
1058+
1059+
context = ValidationContext(
1060+
trust_roots=ca_certs,
1061+
other_certs=other_certs,
1062+
crls=crls,
1063+
allow_fetching=True,
1064+
fetchers=Fetchers(
1065+
ocsp_fetcher=MockOCSPFetcher(),
1066+
crl_fetcher=ReturnPredeterminedCRLs([crl3]),
1067+
cert_fetcher=MockCertFetcher(),
1068+
),
1069+
revocation_mode="require",
1070+
weak_hash_algos={'md2', 'md5'},
1071+
)
1072+
1073+
with pytest.warns(DeprecationWarning):
1074+
retrieved_crls = context.retrieve_crls(cert)
1075+
assert {c.dump() for c in retrieved_crls} == {
1076+
crl1.dump(),
1077+
crl2.dump(),
1078+
crl3.dump(),
1079+
}

0 commit comments

Comments
 (0)