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

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. 

22 

23from cryptography import x509 

24from cryptography.hazmat.primitives.asymmetric import ec 

25 

26 

27class X509CertificateValidator: 

28 def __init__(self, crl_getter): 

29 self._crl_getter = crl_getter 

30 

31 def get_crl_info(self, subject): 

32 crl_getter = self._crl_getter 

33 crl_info = None 

34 

35 try: 

36 crldps = subject.extensions.\ 

37 get_extension_for_class(x509.CRLDistributionPoints).value 

38 

39 if len(crldps) == 0: 

40 raise RuntimeError("No CRL distribution points found in certificate") 

41 

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 

49 

50 if crl_info is None: 

51 raise RuntimeError("None of the distribution points " 

52 "provided a valid CRL") 

53 

54 return crl_info 

55 except Exception as e: 

56 raise RuntimeError(f"Unable to fetch CRL for X509 certificate: {e}") 

57 

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") 

64 

65 warnings = [] 

66 

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") 

70 

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) 

76 

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}") 

85 

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"] 

91 

92 if not crl.is_signature_valid(issuer.public_key()): 

93 raise RuntimeError("Invalid CRL signature from " 

94 f"{issuer.subject}") 

95 

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") 

100 

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}") 

110 

111 if crl_info["warning"] is not None: 

112 warnings.append(crl_info["warning"]) 

113 

114 return { 

115 "valid": True, 

116 "warnings": warnings, 

117 } 

118 except Exception as e: 

119 return { 

120 "valid": False, 

121 "reason": str(e), 

122 }