Coverage for pki_tools/types/chain.py: 73%

51 statements  

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

1import warnings 

2from datetime import datetime 

3 

4import pytz 

5from loguru import logger 

6from pydantic import ConfigDict 

7 

8from pki_tools.exceptions import ( 

9 CertExpired, 

10 CertIssuerMissingInChain, 

11 NotCompleteChain, 

12) 

13from pki_tools.types.certificate import Certificate 

14from pki_tools.types.certificates import Certificates 

15from pki_tools.types.crl import CertificateRevocationList 

16 

17 

18class Chain(Certificates): 

19 """ 

20 Chain holds a list of certificates in a 

21 [chain of trust](https://en.wikipedia.org/wiki/Chain_of_trust) 

22 

23 --8<-- "docs/examples/chain.md" 

24 --8<-- "docs/examples/chain_from_cryptography.md" 

25 --8<-- "docs/examples/chain_from_uri.md" 

26 """ 

27 

28 model_config = ConfigDict(arbitrary_types_allowed=True) 

29 

30 def check_chain(self): 

31 """ 

32 Validate the chain (if it contains more than one certificate) 

33 checking expiration and signatures of all certificates in the chain 

34 

35 .. deprecated:: 

36 Use :meth:`validate_trust_path` instead, which only validates the 

37 trust path relevant to a specific certificate. This method 

38 validates all certificates in the chain, which causes false 

39 failures when the chain contains unrelated expired CAs. 

40 

41 Raises: 

42 NotCompleteChain: When the chain contain only one not self-signed 

43 certificate 

44 CertExpired: If some certificate in the chain has expired 

45 InvalidSignedType: When the issuer has a non-supported type 

46 SignatureVerificationFailed: When the signature verification fails 

47 """ 

48 warnings.warn( 

49 "check_chain() is deprecated, use validate_trust_path(signed) instead. " 

50 "check_chain() validates all certificates in the chain which causes " 

51 "false failures when the chain contains unrelated expired CAs.", 

52 DeprecationWarning, 

53 stacklevel=2, 

54 ) 

55 if len(self.certificates) == 1: 

56 if self.certificates[0].issuer == self.certificates[0].subject: 

57 logger.debug( 

58 "Chain contains only one self signed cert, " 

59 "nothing to check" 

60 ) 

61 return 

62 else: 

63 raise NotCompleteChain() 

64 

65 for cert in self.certificates: 

66 log = logger.bind(subject=cert.subject._string_dict()) 

67 if cert.validity.not_after < datetime.now( 

68 pytz.utc 

69 ) or cert.validity.not_before > datetime.now(pytz.utc): 

70 log.error("Certificate expired") 

71 raise CertExpired( 

72 f"Certificate in chain with subject: '{cert.subject}' " 

73 f"has expired" 

74 ) 

75 

76 issuer = self.get_issuer(cert) 

77 issuer.verify_signature(cert) 

78 

79 def validate_trust_path( 

80 self, 

81 signed: [ 

82 "Certificate", 

83 "CertificateRevocationList", 

84 ], 

85 ): 

86 """ 

87 Validate only the trust path relevant to a given signed entity, 

88 checking expiration and signatures from the issuer up to the root. 

89 

90 Unlike [check_chain][pki_tools.types.chain.Chain.check_chain] which 

91 validates every certificate in the chain, this method only validates 

92 the certificates that form the trust path for `signed`. 

93 

94 Arguments: 

95 signed: The signed entity whose issuer path should be validated 

96 

97 Raises: 

98 CertIssuerMissingInChain: When the issuer of the entity is missing 

99 in the chain 

100 CertExpired: If some certificate in the path has expired 

101 InvalidSignedType: When the issuer has a non-supported type 

102 SignatureVerificationFailed: When the signature verification fails 

103 """ 

104 current = signed 

105 visited = set() 

106 

107 while True: 

108 issuer_cert = self.get_issuer(current) 

109 

110 if issuer_cert.serial_number in visited: 

111 break 

112 visited.add(issuer_cert.serial_number) 

113 

114 # Self-signed root CAs are trust anchors 

115 is_root = issuer_cert.issuer == issuer_cert.subject 

116 

117 # Check expiration only for non-root intermediates 

118 if not is_root: 

119 log = logger.bind(subject=issuer_cert.subject._string_dict()) 

120 if issuer_cert.validity.not_after < datetime.now( 

121 pytz.utc 

122 ) or issuer_cert.validity.not_before > datetime.now(pytz.utc): 

123 log.error("Certificate expired") 

124 raise CertExpired( 

125 f"Certificate in chain with subject: " 

126 f"'{issuer_cert.subject}' has expired" 

127 ) 

128 

129 issuer_cert.verify_signature(current) 

130 

131 if is_root: 

132 break 

133 

134 current = issuer_cert 

135 

136 def get_issuer( 

137 self, 

138 signed: [ 

139 Certificate, 

140 CertificateRevocationList, 

141 ], 

142 ) -> Certificate: 

143 """ 

144 Returns the issuer of a signed entity 

145 

146 Arguments: 

147 signed: The signed entity can either be a 

148 [Certificate][pki_tools.types.certificate.Certificate] or 

149 [CertificateRevocationList][pki_tools.types.crl.CertificateRevocationList] 

150 Returns: 

151 The issuer of the `signed` entity 

152 

153 Raises: 

154 CertIssuerMissingInChain: When the issuer of the entity is missing 

155 in the chain 

156 """ 

157 cert_issuer = signed.issuer 

158 log = logger.bind(issuer=cert_issuer._string_dict()) 

159 

160 for next_chain_cert in self.certificates: 

161 if cert_issuer == next_chain_cert.subject: 

162 log.trace("Found issuer cert in chain") 

163 return next_chain_cert 

164 

165 raise CertIssuerMissingInChain()