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
« prev ^ index » next coverage.py v7.14.3, created at 2026-11-02 22:00 +0000
1import warnings
2from datetime import datetime
4import pytz
5from loguru import logger
6from pydantic import ConfigDict
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
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)
23 --8<-- "docs/examples/chain.md"
24 --8<-- "docs/examples/chain_from_cryptography.md"
25 --8<-- "docs/examples/chain_from_uri.md"
26 """
28 model_config = ConfigDict(arbitrary_types_allowed=True)
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
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.
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()
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 )
76 issuer = self.get_issuer(cert)
77 issuer.verify_signature(cert)
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.
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`.
94 Arguments:
95 signed: The signed entity whose issuer path should be validated
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()
107 while True:
108 issuer_cert = self.get_issuer(current)
110 if issuer_cert.serial_number in visited:
111 break
112 visited.add(issuer_cert.serial_number)
114 # Self-signed root CAs are trust anchors
115 is_root = issuer_cert.issuer == issuer_cert.subject
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 )
129 issuer_cert.verify_signature(current)
131 if is_root:
132 break
134 current = issuer_cert
136 def get_issuer(
137 self,
138 signed: [
139 Certificate,
140 CertificateRevocationList,
141 ],
142 ) -> Certificate:
143 """
144 Returns the issuer of a signed entity
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
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())
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
165 raise CertIssuerMissingInChain()