Skip to content
This repository was archived by the owner on May 23, 2025. It is now read-only.

Commit 39184fb

Browse files
Allow caller to assert revocation status of a cert
1 parent 8bc06cd commit 39184fb

File tree

5 files changed

+92
-8
lines changed

5 files changed

+92
-8
lines changed

pyhanko_certvalidator/context.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
AlgorithmUsagePolicy,
1919
CertRevTrustPolicy,
2020
DisallowWeakAlgorithmsPolicy,
21+
NonRevokedStatusAssertion,
2122
PKIXValidationParams,
2223
RevocationCheckingPolicy,
2324
)
@@ -546,6 +547,7 @@ def bootstrap_validation_data_handlers(
546547
ocsps: Iterable[OCSPContainer] = (),
547548
certs: Iterable[x509.Certificate] = (),
548549
poe_manager: Optional[POEManager] = None,
550+
nonrevoked_assertions: Iterable[NonRevokedStatusAssertion] = (),
549551
) -> ValidationDataHandlers:
550552
"""
551553
Simple bootstrapping method for a :class:`.ValidationDataHandlers`
@@ -564,6 +566,9 @@ def bootstrap_validation_data_handlers(
564566
Initial collection of certificates to add to the certificate registry.
565567
:param poe_manager:
566568
Explicit POE manager. Will instantiate an empty one if left unspecified.
569+
:param nonrevoked_assertions:
570+
Assertions about the non-revoked status of certain certificates
571+
that will be taken as true by fiat.
567572
:return:
568573
A :class:`.ValidationDataHandlers` object.
569574
"""
@@ -587,6 +592,7 @@ def bootstrap_validation_data_handlers(
587592
crls=crls,
588593
ocsps=ocsps,
589594
fetchers=_fetchers,
595+
assertions=nonrevoked_assertions,
590596
)
591597
return ValidationDataHandlers(
592598
revinfo_manager=revinfo_manager,

pyhanko_certvalidator/policy_decl.py

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
import enum
66
from dataclasses import dataclass
77
from datetime import datetime, timedelta
8-
from typing import FrozenSet, Optional
8+
from typing import FrozenSet, Iterable, Optional
99

1010
from asn1crypto import algos, keys
1111

@@ -21,6 +21,7 @@
2121
'AlgorithmUsagePolicy',
2222
'DisallowWeakAlgorithmsPolicy',
2323
'AcceptAllAlgorithms',
24+
'NonRevokedStatusAssertion',
2425
'DEFAULT_WEAK_HASH_ALGOS',
2526
'REQUIRE_REVINFO',
2627
'NO_REVOCATION',
@@ -40,6 +41,23 @@
4041
"""
4142

4243

44+
@dataclass(frozen=True)
45+
class NonRevokedStatusAssertion:
46+
"""
47+
Assert that a certificate was not revoked at some given date.
48+
"""
49+
50+
cert_sha256: bytes
51+
"""
52+
SHA-256 hash of the certificate.
53+
"""
54+
55+
at: datetime
56+
"""
57+
Moment in time at which the assertion is to be considered valid.
58+
"""
59+
60+
4361
@enum.unique
4462
class RevocationCheckingRule(enum.Enum):
4563
"""

pyhanko_certvalidator/revinfo/manager.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
from datetime import datetime
12
from typing import Dict, Iterable, List, Optional, Set
23

34
from asn1crypto import crl, ocsp, x509
@@ -13,6 +14,7 @@
1314
ValidationObjectType,
1415
digest_for_poe,
1516
)
17+
from pyhanko_certvalidator.policy_decl import NonRevokedStatusAssertion
1618
from pyhanko_certvalidator.registry import CertificateRegistry
1719
from pyhanko_certvalidator.revinfo.archival import (
1820
CRLContainer,
@@ -46,6 +48,7 @@ def __init__(
4648
poe_manager: POEManager,
4749
crls: Iterable[CRLContainer],
4850
ocsps: Iterable[OCSPContainer],
51+
assertions: Iterable[NonRevokedStatusAssertion] = (),
4952
fetchers: Optional[Fetchers] = None,
5053
):
5154
self._certificate_registry = certificate_registry
@@ -65,6 +68,9 @@ def __init__(
6568
self._extract_ocsp_certs(ocsp_response)
6669

6770
self._fetchers = fetchers
71+
self._assertions: Dict[bytes, NonRevokedStatusAssertion] = {
72+
assertion.cert_sha256: assertion for assertion in assertions
73+
}
6874

6975
@property
7076
def poe_manager(self) -> POEManager:
@@ -275,3 +281,11 @@ def p(container: CRLContainer):
275281
return digest not in hashes_to_evict
276282

277283
self._crls = list(filter(p, self._crls))
284+
285+
def check_asserted_unrevoked(
286+
self, cert: x509.Certificate, at: datetime
287+
) -> bool:
288+
try:
289+
return at <= self._assertions[cert.sha256].at
290+
except KeyError:
291+
return False

pyhanko_certvalidator/validate.py

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1085,6 +1085,14 @@ async def intl_validate_path(
10851085
else:
10861086
cert = None
10871087

1088+
# TODO support this for attr certs
1089+
leaf_asserted_nonrevoked = False
1090+
revinfo_manager = validation_context.revinfo_manager
1091+
if isinstance(path.leaf, x509.Certificate):
1092+
leaf_asserted_nonrevoked = revinfo_manager.check_asserted_unrevoked(
1093+
path.leaf, moment
1094+
)
1095+
10881096
for index in range(1, path_length + 1):
10891097
cert = path[index]
10901098

@@ -1109,12 +1117,16 @@ async def intl_validate_path(
11091117
)
11101118

11111119
# Step 2 a 3 - CRL/OCSP
1112-
await _check_revocation(
1113-
cert=cert,
1114-
validation_context=validation_context,
1115-
path=path,
1116-
proc_state=proc_state,
1117-
)
1120+
if (
1121+
not leaf_asserted_nonrevoked
1122+
and not revinfo_manager.check_asserted_unrevoked(cert, moment)
1123+
):
1124+
await _check_revocation(
1125+
cert=cert,
1126+
validation_context=validation_context,
1127+
path=path,
1128+
proc_state=proc_state,
1129+
)
11181130

11191131
# Step 2 a 4
11201132
if cert.issuer != state.working_issuer_name:

tests/test_validate.py

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,8 +31,14 @@
3131
aiohttp_fetchers,
3232
requests_fetchers,
3333
)
34+
from pyhanko_certvalidator.ltv.poe import POEManager
3435
from pyhanko_certvalidator.path import QualifiedPolicy, ValidationPath
35-
from pyhanko_certvalidator.policy_decl import DisallowWeakAlgorithmsPolicy
36+
from pyhanko_certvalidator.policy_decl import (
37+
DisallowWeakAlgorithmsPolicy,
38+
NonRevokedStatusAssertion,
39+
)
40+
from pyhanko_certvalidator.registry import CertificateRegistry
41+
from pyhanko_certvalidator.revinfo.manager import RevinfoManager
3642
from pyhanko_certvalidator.validate import async_validate_path, validate_path
3743

3844
from .common import (
@@ -352,6 +358,34 @@ def test_ed448():
352358
validate_path(context, path)
353359

354360

361+
def test_assert_no_revinfo_needed_by_fiat():
362+
cert = load_cert_object('testing-ca-pss', 'signer1.cert.pem')
363+
ca_certs = [load_cert_object('testing-ca-pss', 'root.cert.pem')]
364+
other_certs = [load_cert_object('testing-ca-pss', 'interm.cert.pem')]
365+
moment = datetime(2021, 5, 3, tzinfo=timezone.utc)
366+
assertion = NonRevokedStatusAssertion(cert.sha256, moment)
367+
revinfo_manager = RevinfoManager(
368+
certificate_registry=CertificateRegistry.build(),
369+
poe_manager=POEManager(),
370+
crls=(),
371+
ocsps=(),
372+
assertions=(assertion,),
373+
)
374+
context = ValidationContext(
375+
trust_roots=ca_certs,
376+
other_certs=other_certs,
377+
allow_fetching=False,
378+
moment=moment,
379+
revocation_mode='require', # turn on strict revinfovalidation
380+
revinfo_manager=revinfo_manager,
381+
)
382+
paths = context.path_builder.build_paths(cert)
383+
assert 1 == len(paths)
384+
path = paths[0]
385+
assert 3 == len(path)
386+
validate_path(context, path)
387+
388+
355389
def test_multitasking_ocsp():
356390
# regression test for case where the same responder ID (name + key ID)
357391
# is used in OCSP responses for different issuers in the same chain of

0 commit comments

Comments
 (0)