Coverage for admin/x509_validator.py: 100%
54 statements
« prev ^ index » next coverage.py v7.5.3, created at 2025-10-30 06:22 +0000
« prev ^ index » next coverage.py v7.5.3, created at 2025-10-30 06:22 +0000
1# The MIT License (MIT)
2#
3# Copyright (c) 2021 RSK Labs Ltd
4#
5# Permission is hereby granted, free of charge, to any person obtaining a copy of
6# this software and associated documentation files (the "Software"), to deal in
7# the Software without restriction, including without limitation the rights to
8# use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
9# of the Software, and to permit persons to whom the Software is furnished to do
10# so, subject to the following conditions:
11#
12# The above copyright notice and this permission notice shall be included in all
13# copies or substantial portions of the Software.
14#
15# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21# SOFTWARE.
23from cryptography import x509
24from cryptography.hazmat.primitives.asymmetric import ec
27class X509CertificateValidator:
28 def __init__(self, crl_getter):
29 self._crl_getter = crl_getter
31 def get_crl_info(self, subject):
32 crl_getter = self._crl_getter
33 crl_info = None
35 try:
36 crldps = subject.extensions.\
37 get_extension_for_class(x509.CRLDistributionPoints).value
39 if len(crldps) == 0:
40 raise RuntimeError("No CRL distribution points found in certificate")
42 for crldp in crldps:
43 url = crldp.full_name[0].value
44 try:
45 crl_info = crl_getter(url)
46 break
47 except RuntimeError:
48 pass
50 if crl_info is None:
51 raise RuntimeError("None of the distribution points "
52 "provided a valid CRL")
54 return crl_info
55 except Exception as e:
56 raise RuntimeError(f"Unable to fetch CRL for X509 certificate: {e}")
58 def validate(self, subject, issuer, now, check_crl=True):
59 try:
60 if not isinstance(subject, x509.Certificate) or \
61 not isinstance(issuer, x509.Certificate):
62 raise RuntimeError("Both subject and issuer must be "
63 "instances of x509.Certificate")
65 warnings = []
67 # 1. Check validity period
68 if subject.not_valid_before_utc > now or subject.not_valid_after_utc < now:
69 raise RuntimeError(f"{subject.subject} not within validity period")
71 # 2. Verify directly issued by issuer and
72 # also manually verify the signature just to
73 # have a second opinion XD
74 try:
75 subject.verify_directly_issued_by(issuer)
77 issuer.public_key().verify(
78 subject.signature,
79 subject.tbs_certificate_bytes,
80 ec.ECDSA(subject.signature_hash_algorithm)
81 )
82 except Exception as e:
83 raise RuntimeError(f"Verifying {subject.subject} issued "
84 f"by {issuer.subject}: {e}")
86 # 3. Gather CRL and check validity
87 # (can be skipped for e.g. root of trust)
88 if check_crl:
89 crl_info = self.get_crl_info(subject)
90 crl = crl_info["crl"]
92 if not crl.is_signature_valid(issuer.public_key()):
93 raise RuntimeError("Invalid CRL signature from "
94 f"{issuer.subject}")
96 revoked = crl.get_revoked_certificate_by_serial_number(
97 subject.serial_number)
98 if revoked is not None:
99 raise RuntimeError(f"{subject.subject} found in {issuer.subject} CRL")
101 # If the CRL issuer chain is present, check that the first
102 # element (the leaf) matches the given issuer
103 issuer_chain = crl_info["issuer_chain"]
104 if issuer_chain is not None:
105 leaf = issuer_chain[0]
106 if leaf != issuer:
107 raise RuntimeError(f"CRL issuer chain leaf {leaf.subject} does "
108 f"not match certificate {subject.subject} "
109 f"issuer {issuer.subject}")
111 if crl_info["warning"] is not None:
112 warnings.append(crl_info["warning"])
114 return {
115 "valid": True,
116 "warnings": warnings,
117 }
118 except Exception as e:
119 return {
120 "valid": False,
121 "reason": str(e),
122 }