Coverage for pki_tools/crl.py: 95%

81 statements  

« prev     ^ index     » next       coverage.py v7.14.3, created at 2026-11-02 22:00 +0000

1import socket 

2from urllib.parse import urlparse 

3 

4from loguru import logger 

5 

6from pki_tools.exceptions import ( 

7 CrlIdpInvalid, 

8 ExtensionMissing, 

9 FetchFailure, 

10 LoadError, 

11) 

12from pki_tools.types.certificate import Certificate 

13from pki_tools.types.chain import Chain 

14from pki_tools.types.crl import CertificateRevocationList 

15from pki_tools.types.extensions import UniformResourceIdentifier 

16 

17 

18def _compare_cdp_and_idp( 

19 cdp_uri: str, idp_uri: str, same_crl_domains: list[list[str]] | None = None 

20): 

21 log = logger.bind( 

22 cdp=cdp_uri, 

23 idp=idp_uri, 

24 ) 

25 

26 parsed_cdp = urlparse(cdp_uri) 

27 parsed_idp = urlparse(idp_uri) 

28 

29 if ( 

30 parsed_cdp.path.rstrip("/").split("/")[-1] 

31 != parsed_idp.path.rstrip("/").split("/")[-1] 

32 ): 

33 log.warning("CRL IDP path is not the same as cert CDP") 

34 return False 

35 

36 if parsed_cdp.scheme != parsed_idp.scheme: 

37 log.warning("CRL IDP scheme is not the same as cert CDP") 

38 return False 

39 

40 if same_crl_domains is not None: 

41 for domain_list in same_crl_domains: 

42 if ( 

43 parsed_cdp.hostname in domain_list 

44 and parsed_idp.hostname in domain_list 

45 ): 

46 log.debug( 

47 "CRL IDP and cert CDP are considered to be the same domain" 

48 ) 

49 return True 

50 

51 if parsed_cdp.hostname != parsed_idp.hostname: 

52 try: 

53 cdp_ips = set(socket.gethostbyname_ex(parsed_cdp.hostname)[2]) 

54 idp_ips = set(socket.gethostbyname_ex(parsed_idp.hostname)[2]) 

55 except socket.gaierror: 

56 log.warning( 

57 "CRL IDP or cert CDP hostname not resolvable and differ" 

58 ) 

59 return False 

60 

61 if not (cdp_ips <= idp_ips or idp_ips <= cdp_ips): 

62 # Not (All elements of one set are in the other) 

63 log.bind( 

64 cdp_ips=",".join(cdp_ips), 

65 idp_ips=",".join(idp_ips), 

66 ).warning("CRL IDP hostname is not the same as cert CDP") 

67 return False 

68 

69 return True 

70 

71 

72def _is_revoked( 

73 cert: Certificate, 

74 crl_issuer: Chain, 

75 crl_cache_seconds: int = 3600, 

76 same_crl_domains: list[list[str]] | None = None, 

77) -> bool: 

78 crl_issuer.validate_trust_path(cert) 

79 logger.debug("CRL issuer chain valid for cert") 

80 

81 log = logger.bind( 

82 cert=cert.pem_string, 

83 serial=cert.hex_serial, 

84 ) 

85 

86 if cert.extensions.crl_distribution_points is None: 

87 log.debug("CRL extension missing") 

88 raise ExtensionMissing() 

89 

90 http_dist = False 

91 fetched_crl = False 

92 last_fetched_crl_exception = None 

93 

94 for dist_point in cert.extensions.crl_distribution_points: 

95 if dist_point.full_name is None: 

96 continue 

97 

98 for full_name in dist_point.full_name: 

99 if not isinstance(full_name, UniformResourceIdentifier): 

100 log.warning( 

101 "CRL Distribution Point is not UniformResourceIdentifier" 

102 ) 

103 continue 

104 

105 uri = full_name.value 

106 

107 http_dist = True 

108 

109 try: 

110 crl = CertificateRevocationList.from_uri( 

111 uri, cache_time_seconds=crl_cache_seconds 

112 ) 

113 except (LoadError, FetchFailure) as e: 

114 log.warning("Failed to fetch CRL from uri") 

115 last_fetched_crl_exception = e 

116 continue 

117 

118 fetched_crl = True 

119 

120 issuer = crl_issuer.get_issuer(crl) 

121 

122 issuer.verify_signature(crl) 

123 log.debug("CRL signature valid") 

124 

125 if ( 

126 crl.extensions is not None 

127 and crl.extensions.issuing_distribution_point is not None 

128 and crl.extensions.issuing_distribution_point.full_name 

129 is not None 

130 ): 

131 log.debug("CRL Issuing Distribution Point extension present") 

132 full_names = ( 

133 crl.extensions.issuing_distribution_point.full_name 

134 ) 

135 

136 for crl_idp in full_names: 

137 if _compare_cdp_and_idp( 

138 uri, crl_idp.value, same_crl_domains=same_crl_domains 

139 ): 

140 break 

141 else: 

142 log.error("CRL IDP and cert CDP differ") 

143 raise CrlIdpInvalid() 

144 

145 if (r := crl.get_revoked(cert.serial_number)) is not None: 

146 log.bind(date=str(r.date)).debug("Certificate revoked") 

147 return True 

148 

149 if not http_dist: 

150 log.debug("CRL missing URI") 

151 raise ExtensionMissing() 

152 

153 if not fetched_crl and isinstance(last_fetched_crl_exception, Exception): 

154 raise last_fetched_crl_exception 

155 

156 log.debug("Certificate valid") 

157 return False