certgraph 0.3.0__tar.gz → 0.4.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (25) hide show
  1. certgraph-0.4.0/PKG-INFO +169 -0
  2. certgraph-0.4.0/README.md +129 -0
  3. {certgraph-0.3.0 → certgraph-0.4.0}/pyproject.toml +1 -1
  4. {certgraph-0.3.0 → certgraph-0.4.0}/src/certgraph/certgraph.py +49 -5
  5. certgraph-0.4.0/src/certgraph.egg-info/PKG-INFO +169 -0
  6. {certgraph-0.3.0 → certgraph-0.4.0}/src/certgraph.egg-info/SOURCES.txt +1 -0
  7. certgraph-0.4.0/tests/test_certificate_invalid_at.py +39 -0
  8. {certgraph-0.3.0 → certgraph-0.4.0}/tests/test_export_dot.py +17 -0
  9. certgraph-0.3.0/PKG-INFO +0 -42
  10. certgraph-0.3.0/README.md +0 -2
  11. certgraph-0.3.0/src/certgraph.egg-info/PKG-INFO +0 -42
  12. {certgraph-0.3.0 → certgraph-0.4.0}/LICENSE +0 -0
  13. {certgraph-0.3.0 → certgraph-0.4.0}/setup.cfg +0 -0
  14. {certgraph-0.3.0 → certgraph-0.4.0}/src/certgraph/__init__.py +0 -0
  15. {certgraph-0.3.0 → certgraph-0.4.0}/src/certgraph.egg-info/dependency_links.txt +0 -0
  16. {certgraph-0.3.0 → certgraph-0.4.0}/src/certgraph.egg-info/requires.txt +0 -0
  17. {certgraph-0.3.0 → certgraph-0.4.0}/src/certgraph.egg-info/top_level.txt +0 -0
  18. {certgraph-0.3.0 → certgraph-0.4.0}/tests/test_clear.py +0 -0
  19. {certgraph-0.3.0 → certgraph-0.4.0}/tests/test_fingerprint_search.py +0 -0
  20. {certgraph-0.3.0 → certgraph-0.4.0}/tests/test_get_certificate.py +0 -0
  21. {certgraph-0.3.0 → certgraph-0.4.0}/tests/test_get_child_fingerprints.py +0 -0
  22. {certgraph-0.3.0 → certgraph-0.4.0}/tests/test_get_issuer_fingerprint.py +0 -0
  23. {certgraph-0.3.0 → certgraph-0.4.0}/tests/test_graph.py +0 -0
  24. {certgraph-0.3.0 → certgraph-0.4.0}/tests/test_import_certificates.py +0 -0
  25. {certgraph-0.3.0 → certgraph-0.4.0}/tests/test_remove_certificate.py +0 -0
@@ -0,0 +1,169 @@
1
+ Metadata-Version: 2.4
2
+ Name: certgraph
3
+ Version: 0.4.0
4
+ Summary: Utility for exploring and mapping X509 certificate chains.
5
+ Author: Chris Adshead
6
+ License: MIT License
7
+
8
+ Copyright (c) 2026 Chris Adshead
9
+
10
+ Permission is hereby granted, free of charge, to any person obtaining a copy
11
+ of this software and associated documentation files (the "Software"), to deal
12
+ in the Software without restriction, including without limitation the rights
13
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
14
+ copies of the Software, and to permit persons to whom the Software is
15
+ furnished to do so, subject to the following conditions:
16
+
17
+ The above copyright notice and this permission notice shall be included in all
18
+ copies or substantial portions of the Software.
19
+
20
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
21
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
22
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
23
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
24
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
25
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
26
+ SOFTWARE.
27
+
28
+ Requires-Python: >=3.12
29
+ Description-Content-Type: text/markdown
30
+ License-File: LICENSE
31
+ Requires-Dist: cryptography
32
+ Requires-Dist: networkx
33
+ Requires-Dist: pydot
34
+ Requires-Dist: rapidfuzz
35
+ Provides-Extra: dev
36
+ Requires-Dist: black; extra == "dev"
37
+ Requires-Dist: pytest; extra == "dev"
38
+ Requires-Dist: pytest-cov; extra == "dev"
39
+ Dynamic: license-file
40
+
41
+ # certgraph
42
+
43
+ [![PyPI version](https://img.shields.io/pypi/v/certgraph)](https://pypi.org/project/certgraph/)
44
+ [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
45
+
46
+ Python package for creating, exploring and displaying directed graphs of X.509 certificates.
47
+
48
+ Build a directed graph by importing PEM/DER/`x509.Certificate` objects using subject/issuer relationships. Explore the
49
+ generated digraph with the built in functions and then export the graph rendered in the DOT language.
50
+
51
+ Basic example graph generated to DOT source with `certgraph.export_dot()` and then rendered to SVG with `dot` command line tool:
52
+
53
+ ![Basic example certificate graph](readme_basic_example.svg)
54
+
55
+ ## Features
56
+
57
+ * Import certificates from:
58
+ * PEM strings (individually or whole PKIs)
59
+ * DER bytes
60
+ * `cryptography.x509.Certificate` objects
61
+ * Dynamically built directed graphs from imported certificates
62
+ * Imported certificates fuzzy-searchable based on distinguised name
63
+ * Issuer and child certificates walkable to follow chains
64
+ * Identify certificates not valid at a given time
65
+ * Export DOT language graphs, with:
66
+ * RFC4514 distinguised names
67
+ * Valid time range
68
+ * Node highlighting for invalid certificates
69
+
70
+ **Note on _fingerprints_**: This code relies extensively on certificate _fingerprints_. This is a SHA256 hash of the public bytes in the certificate. Nodes in the graph are stored with the fingerprint as their key and are indexed using the fingerprint value. While this initially seems awkward, it prevents malformed (or maliciously designed) distinguished names or serial numbers from clashing with other imported certificates. You can use `certgraph.fingerprint_from_distinguished_name()` to map from distinguised names to fingerprints for ease of use.
71
+
72
+ ## Installation
73
+
74
+ ### PyPI
75
+
76
+ Install the latest version of the package from PyPI via `pip` with:
77
+
78
+ ```bash
79
+ pip install certgraph
80
+ ```
81
+
82
+ For development dependencies, install with:
83
+
84
+ ```bash
85
+ pip install certgraph[dev]
86
+ ```
87
+
88
+ ### Optional
89
+
90
+ * `dot` command line tool
91
+ * Used to render generated DOT language graphs into PNG/SVG/etc.
92
+ * E.g. `dot -Tsvg <generated_file>.dot -o graph.svg`
93
+ * Alternatively can use online tools.
94
+
95
+ ## Basic Examples
96
+
97
+ Import the `certgraph` class from the module:
98
+
99
+ ```python
100
+ >>> from certgraph import certgraph as cg
101
+ ```
102
+
103
+ Load some varied data for later examples:
104
+
105
+ ```python
106
+ >>> with open("path/to/cert.pem") as f:
107
+ >>> pem_data = f.read() # 1 cert
108
+ >>> with open("path/to/pki.pem") as f:
109
+ >>> pki_data = f.read() # 3 certs in 1 PEM file
110
+ >>> with open("path/to/cert.der", "rb") as f:
111
+ >>> der_data = f.read() # 1 cert
112
+ ```
113
+
114
+ Import a PEM file with 1 certificate in it:
115
+
116
+ ```python
117
+ >>> graph = cg().import_certificates(pem_data)
118
+ >>> len(graph)
119
+ 1
120
+ ```
121
+
122
+ Import mixed PEM/DER dataset, including PEMs with PKIs, using function chaining:
123
+
124
+ ```python
125
+ >>> graph = (
126
+ >>> cg()
127
+ >>> .import_certificates(pem_data)
128
+ >>> .import_certificates(pki_data)
129
+ >>> .import_certificates(der_data)
130
+ >>> )
131
+ >>> len(graph)
132
+ 5
133
+ ```
134
+
135
+ Get an imported certificate (as `cryptography.x509.Certificate`) by using a fingerprint. The fingerprint of the desired certificate can be retrieved using a fuzzy-searching function based on the distinguised name:
136
+
137
+ ```python
138
+ >>> dn = "CN=f3ec133b-4"
139
+ >>> fingerprint = cg.fingerprint_from_distinguished_name(dn) # Fuzzy-search the distinguised name in the list of imported certificates for the best match
140
+ >>> print(fingerprint)
141
+ "77c4b5b12b79887b8c709df826cff89a7268071593de5cc84053b8c00302b2fb"
142
+ >>> cert = cg.get_certificate(fingerprint) # Get the certificate associated with the fingerprint
143
+ >>> print(cert.subject.rfc4514_string())
144
+ "CN=f3ec133b-4"
145
+ ```
146
+
147
+ Get predecessor (issuer) and successor (child) fingerprints from a certificate fingerprint:
148
+
149
+ ```python
150
+ >>> fingerprint = "77c4b5b12b79887b8c709df826cff89a7268071593de5cc84053b8c00302b2fb"
151
+ >>> cg.get_issuer_fingerprint(fingerprint)
152
+ "965e8d87f96ccf564726ff5e97ea4d6b18731d9b762ef9510479f628bc85837e"
153
+ >>> cg.get_child_certificates(fingerprint)
154
+ ["3cf372288714b52298222bb1f8cf51dce3f1e60147ec53f9ee3dc28e8766c734", "2caf1b256bcbb327aea8d1442ba12e0fc6582b3c2980ee9f31d4ae3a62f62673"]
155
+ ```
156
+
157
+ Export graph to DOT language and write to a file, where it can be rendered with the `dot` command line tool.
158
+
159
+ ```python
160
+ >>> with open("path/to/output.dot", "w") as f:
161
+ >>> dot_str = cg.export_dot()
162
+ >>> f.write(dot_str)
163
+ ```
164
+
165
+ See more complex examples in the `examples` directory of the repository (**WIP**).
166
+
167
+ ## Documentation
168
+
169
+ **WIP**
@@ -0,0 +1,129 @@
1
+ # certgraph
2
+
3
+ [![PyPI version](https://img.shields.io/pypi/v/certgraph)](https://pypi.org/project/certgraph/)
4
+ [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
5
+
6
+ Python package for creating, exploring and displaying directed graphs of X.509 certificates.
7
+
8
+ Build a directed graph by importing PEM/DER/`x509.Certificate` objects using subject/issuer relationships. Explore the
9
+ generated digraph with the built in functions and then export the graph rendered in the DOT language.
10
+
11
+ Basic example graph generated to DOT source with `certgraph.export_dot()` and then rendered to SVG with `dot` command line tool:
12
+
13
+ ![Basic example certificate graph](readme_basic_example.svg)
14
+
15
+ ## Features
16
+
17
+ * Import certificates from:
18
+ * PEM strings (individually or whole PKIs)
19
+ * DER bytes
20
+ * `cryptography.x509.Certificate` objects
21
+ * Dynamically built directed graphs from imported certificates
22
+ * Imported certificates fuzzy-searchable based on distinguised name
23
+ * Issuer and child certificates walkable to follow chains
24
+ * Identify certificates not valid at a given time
25
+ * Export DOT language graphs, with:
26
+ * RFC4514 distinguised names
27
+ * Valid time range
28
+ * Node highlighting for invalid certificates
29
+
30
+ **Note on _fingerprints_**: This code relies extensively on certificate _fingerprints_. This is a SHA256 hash of the public bytes in the certificate. Nodes in the graph are stored with the fingerprint as their key and are indexed using the fingerprint value. While this initially seems awkward, it prevents malformed (or maliciously designed) distinguished names or serial numbers from clashing with other imported certificates. You can use `certgraph.fingerprint_from_distinguished_name()` to map from distinguised names to fingerprints for ease of use.
31
+
32
+ ## Installation
33
+
34
+ ### PyPI
35
+
36
+ Install the latest version of the package from PyPI via `pip` with:
37
+
38
+ ```bash
39
+ pip install certgraph
40
+ ```
41
+
42
+ For development dependencies, install with:
43
+
44
+ ```bash
45
+ pip install certgraph[dev]
46
+ ```
47
+
48
+ ### Optional
49
+
50
+ * `dot` command line tool
51
+ * Used to render generated DOT language graphs into PNG/SVG/etc.
52
+ * E.g. `dot -Tsvg <generated_file>.dot -o graph.svg`
53
+ * Alternatively can use online tools.
54
+
55
+ ## Basic Examples
56
+
57
+ Import the `certgraph` class from the module:
58
+
59
+ ```python
60
+ >>> from certgraph import certgraph as cg
61
+ ```
62
+
63
+ Load some varied data for later examples:
64
+
65
+ ```python
66
+ >>> with open("path/to/cert.pem") as f:
67
+ >>> pem_data = f.read() # 1 cert
68
+ >>> with open("path/to/pki.pem") as f:
69
+ >>> pki_data = f.read() # 3 certs in 1 PEM file
70
+ >>> with open("path/to/cert.der", "rb") as f:
71
+ >>> der_data = f.read() # 1 cert
72
+ ```
73
+
74
+ Import a PEM file with 1 certificate in it:
75
+
76
+ ```python
77
+ >>> graph = cg().import_certificates(pem_data)
78
+ >>> len(graph)
79
+ 1
80
+ ```
81
+
82
+ Import mixed PEM/DER dataset, including PEMs with PKIs, using function chaining:
83
+
84
+ ```python
85
+ >>> graph = (
86
+ >>> cg()
87
+ >>> .import_certificates(pem_data)
88
+ >>> .import_certificates(pki_data)
89
+ >>> .import_certificates(der_data)
90
+ >>> )
91
+ >>> len(graph)
92
+ 5
93
+ ```
94
+
95
+ Get an imported certificate (as `cryptography.x509.Certificate`) by using a fingerprint. The fingerprint of the desired certificate can be retrieved using a fuzzy-searching function based on the distinguised name:
96
+
97
+ ```python
98
+ >>> dn = "CN=f3ec133b-4"
99
+ >>> fingerprint = cg.fingerprint_from_distinguished_name(dn) # Fuzzy-search the distinguised name in the list of imported certificates for the best match
100
+ >>> print(fingerprint)
101
+ "77c4b5b12b79887b8c709df826cff89a7268071593de5cc84053b8c00302b2fb"
102
+ >>> cert = cg.get_certificate(fingerprint) # Get the certificate associated with the fingerprint
103
+ >>> print(cert.subject.rfc4514_string())
104
+ "CN=f3ec133b-4"
105
+ ```
106
+
107
+ Get predecessor (issuer) and successor (child) fingerprints from a certificate fingerprint:
108
+
109
+ ```python
110
+ >>> fingerprint = "77c4b5b12b79887b8c709df826cff89a7268071593de5cc84053b8c00302b2fb"
111
+ >>> cg.get_issuer_fingerprint(fingerprint)
112
+ "965e8d87f96ccf564726ff5e97ea4d6b18731d9b762ef9510479f628bc85837e"
113
+ >>> cg.get_child_certificates(fingerprint)
114
+ ["3cf372288714b52298222bb1f8cf51dce3f1e60147ec53f9ee3dc28e8766c734", "2caf1b256bcbb327aea8d1442ba12e0fc6582b3c2980ee9f31d4ae3a62f62673"]
115
+ ```
116
+
117
+ Export graph to DOT language and write to a file, where it can be rendered with the `dot` command line tool.
118
+
119
+ ```python
120
+ >>> with open("path/to/output.dot", "w") as f:
121
+ >>> dot_str = cg.export_dot()
122
+ >>> f.write(dot_str)
123
+ ```
124
+
125
+ See more complex examples in the `examples` directory of the repository (**WIP**).
126
+
127
+ ## Documentation
128
+
129
+ **WIP**
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "certgraph"
7
- version = "0.3.0"
7
+ version = "0.4.0"
8
8
  description = "Utility for exploring and mapping X509 certificate chains."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.12"
@@ -1,5 +1,6 @@
1
1
  from cryptography import x509
2
2
  from cryptography.hazmat.primitives import hashes
3
+ from datetime import datetime, timezone
3
4
  import networkx as nx
4
5
  from rapidfuzz import fuzz
5
6
 
@@ -12,6 +13,10 @@ class certgraph:
12
13
  self._certlist: set[x509.Certificate] = []
13
14
  self._graph = nx.DiGraph()
14
15
 
16
+ def __len__(self) -> int:
17
+ """Get number of certificates imported."""
18
+ return len(self._certlist)
19
+
15
20
  def import_certificates(
16
21
  self,
17
22
  certificates: (
@@ -100,11 +105,23 @@ class certgraph:
100
105
  for fingerprint, data in self._graph.nodes(data=True):
101
106
  cert: x509.Certificate = data["certificate"]
102
107
  validity_time_str = f"{cert.not_valid_before_utc.isoformat()} → {cert.not_valid_after_utc.isoformat()}"
108
+ invalid = self.certificate_invalid_at(fingerprint)
103
109
 
104
110
  label = f"DN: {cert.subject.rfc4514_string()}"
105
111
  label += f"\nValid: {validity_time_str}"
106
112
 
107
- dot_graph.add_node(fingerprint, label=label, shape="box")
113
+ if invalid:
114
+ node_color = "tomato"
115
+ else:
116
+ node_color = "white"
117
+
118
+ dot_graph.add_node(
119
+ fingerprint,
120
+ label=label,
121
+ shape="box",
122
+ style="filled",
123
+ fillcolor=node_color,
124
+ )
108
125
 
109
126
  dot_graph.add_edges_from(self._graph.edges())
110
127
 
@@ -172,7 +189,7 @@ class certgraph:
172
189
 
173
190
  Args:
174
191
  fingerprint: Fingerprint of the certificate to get the issuer of.
175
-
192
+
176
193
  Returns:
177
194
  The fingerprint of the issuing certificate or None if not present in the digraph.
178
195
 
@@ -180,7 +197,9 @@ class certgraph:
180
197
  ValueError: If certificate with the supplied fingerprint isn't in the digraph.
181
198
  """
182
199
  if fingerprint not in self._graph.nodes:
183
- raise ValueError(f"Fingerprint supplied doesn't match any imported certificate.")
200
+ raise ValueError(
201
+ f"Fingerprint supplied doesn't match any imported certificate."
202
+ )
184
203
 
185
204
  return next(self._graph.predecessors(fingerprint), None)
186
205
 
@@ -190,7 +209,7 @@ class certgraph:
190
209
 
191
210
  Args:
192
211
  fingerprint: Fingerprint of the certificate to get the child certificates of.
193
-
212
+
194
213
  Returns:
195
214
  The list of fingerprints belonging to child certificates.
196
215
 
@@ -198,7 +217,9 @@ class certgraph:
198
217
  ValueError: If certificate with the supplied fingerprint isn't in the digraph.
199
218
  """
200
219
  if fingerprint not in self._graph.nodes:
201
- raise ValueError(f"Fingerprint supplied doesn't match any imported certificate.")
220
+ raise ValueError(
221
+ f"Fingerprint supplied doesn't match any imported certificate."
222
+ )
202
223
 
203
224
  return list(self._graph.successors(fingerprint))
204
225
 
@@ -217,3 +238,26 @@ class certgraph:
217
238
  if cert in self._certlist:
218
239
  self._certlist.remove(cert)
219
240
 
241
+ def certificate_invalid_at(
242
+ self, fingerprint: str, at_time: datetime = datetime.now(timezone.utc)
243
+ ) -> bool:
244
+ """
245
+ Check if an imported certificate is outside its validity period - either not yet valid or expired.
246
+
247
+ Args:
248
+ fingerprint: Fingerprint of the certificate to check.
249
+ at_time: When the certificate validity will be checked against. Defaults to now (UTC).
250
+
251
+ Returns:
252
+ True if the certificate is not yet valid or expired at the given time, otherwise false.
253
+
254
+ Raises:
255
+ ValueError: If certificate with the supplied fingerprint isn't in the digraph.
256
+ """
257
+ if fingerprint not in self._graph.nodes:
258
+ raise ValueError(
259
+ f"Fingerprint supplied doesn't match any imported certificate."
260
+ )
261
+
262
+ cert = self.get_certificate(fingerprint)
263
+ return at_time < cert.not_valid_before_utc or at_time > cert.not_valid_after_utc
@@ -0,0 +1,169 @@
1
+ Metadata-Version: 2.4
2
+ Name: certgraph
3
+ Version: 0.4.0
4
+ Summary: Utility for exploring and mapping X509 certificate chains.
5
+ Author: Chris Adshead
6
+ License: MIT License
7
+
8
+ Copyright (c) 2026 Chris Adshead
9
+
10
+ Permission is hereby granted, free of charge, to any person obtaining a copy
11
+ of this software and associated documentation files (the "Software"), to deal
12
+ in the Software without restriction, including without limitation the rights
13
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
14
+ copies of the Software, and to permit persons to whom the Software is
15
+ furnished to do so, subject to the following conditions:
16
+
17
+ The above copyright notice and this permission notice shall be included in all
18
+ copies or substantial portions of the Software.
19
+
20
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
21
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
22
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
23
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
24
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
25
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
26
+ SOFTWARE.
27
+
28
+ Requires-Python: >=3.12
29
+ Description-Content-Type: text/markdown
30
+ License-File: LICENSE
31
+ Requires-Dist: cryptography
32
+ Requires-Dist: networkx
33
+ Requires-Dist: pydot
34
+ Requires-Dist: rapidfuzz
35
+ Provides-Extra: dev
36
+ Requires-Dist: black; extra == "dev"
37
+ Requires-Dist: pytest; extra == "dev"
38
+ Requires-Dist: pytest-cov; extra == "dev"
39
+ Dynamic: license-file
40
+
41
+ # certgraph
42
+
43
+ [![PyPI version](https://img.shields.io/pypi/v/certgraph)](https://pypi.org/project/certgraph/)
44
+ [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
45
+
46
+ Python package for creating, exploring and displaying directed graphs of X.509 certificates.
47
+
48
+ Build a directed graph by importing PEM/DER/`x509.Certificate` objects using subject/issuer relationships. Explore the
49
+ generated digraph with the built in functions and then export the graph rendered in the DOT language.
50
+
51
+ Basic example graph generated to DOT source with `certgraph.export_dot()` and then rendered to SVG with `dot` command line tool:
52
+
53
+ ![Basic example certificate graph](readme_basic_example.svg)
54
+
55
+ ## Features
56
+
57
+ * Import certificates from:
58
+ * PEM strings (individually or whole PKIs)
59
+ * DER bytes
60
+ * `cryptography.x509.Certificate` objects
61
+ * Dynamically built directed graphs from imported certificates
62
+ * Imported certificates fuzzy-searchable based on distinguised name
63
+ * Issuer and child certificates walkable to follow chains
64
+ * Identify certificates not valid at a given time
65
+ * Export DOT language graphs, with:
66
+ * RFC4514 distinguised names
67
+ * Valid time range
68
+ * Node highlighting for invalid certificates
69
+
70
+ **Note on _fingerprints_**: This code relies extensively on certificate _fingerprints_. This is a SHA256 hash of the public bytes in the certificate. Nodes in the graph are stored with the fingerprint as their key and are indexed using the fingerprint value. While this initially seems awkward, it prevents malformed (or maliciously designed) distinguished names or serial numbers from clashing with other imported certificates. You can use `certgraph.fingerprint_from_distinguished_name()` to map from distinguised names to fingerprints for ease of use.
71
+
72
+ ## Installation
73
+
74
+ ### PyPI
75
+
76
+ Install the latest version of the package from PyPI via `pip` with:
77
+
78
+ ```bash
79
+ pip install certgraph
80
+ ```
81
+
82
+ For development dependencies, install with:
83
+
84
+ ```bash
85
+ pip install certgraph[dev]
86
+ ```
87
+
88
+ ### Optional
89
+
90
+ * `dot` command line tool
91
+ * Used to render generated DOT language graphs into PNG/SVG/etc.
92
+ * E.g. `dot -Tsvg <generated_file>.dot -o graph.svg`
93
+ * Alternatively can use online tools.
94
+
95
+ ## Basic Examples
96
+
97
+ Import the `certgraph` class from the module:
98
+
99
+ ```python
100
+ >>> from certgraph import certgraph as cg
101
+ ```
102
+
103
+ Load some varied data for later examples:
104
+
105
+ ```python
106
+ >>> with open("path/to/cert.pem") as f:
107
+ >>> pem_data = f.read() # 1 cert
108
+ >>> with open("path/to/pki.pem") as f:
109
+ >>> pki_data = f.read() # 3 certs in 1 PEM file
110
+ >>> with open("path/to/cert.der", "rb") as f:
111
+ >>> der_data = f.read() # 1 cert
112
+ ```
113
+
114
+ Import a PEM file with 1 certificate in it:
115
+
116
+ ```python
117
+ >>> graph = cg().import_certificates(pem_data)
118
+ >>> len(graph)
119
+ 1
120
+ ```
121
+
122
+ Import mixed PEM/DER dataset, including PEMs with PKIs, using function chaining:
123
+
124
+ ```python
125
+ >>> graph = (
126
+ >>> cg()
127
+ >>> .import_certificates(pem_data)
128
+ >>> .import_certificates(pki_data)
129
+ >>> .import_certificates(der_data)
130
+ >>> )
131
+ >>> len(graph)
132
+ 5
133
+ ```
134
+
135
+ Get an imported certificate (as `cryptography.x509.Certificate`) by using a fingerprint. The fingerprint of the desired certificate can be retrieved using a fuzzy-searching function based on the distinguised name:
136
+
137
+ ```python
138
+ >>> dn = "CN=f3ec133b-4"
139
+ >>> fingerprint = cg.fingerprint_from_distinguished_name(dn) # Fuzzy-search the distinguised name in the list of imported certificates for the best match
140
+ >>> print(fingerprint)
141
+ "77c4b5b12b79887b8c709df826cff89a7268071593de5cc84053b8c00302b2fb"
142
+ >>> cert = cg.get_certificate(fingerprint) # Get the certificate associated with the fingerprint
143
+ >>> print(cert.subject.rfc4514_string())
144
+ "CN=f3ec133b-4"
145
+ ```
146
+
147
+ Get predecessor (issuer) and successor (child) fingerprints from a certificate fingerprint:
148
+
149
+ ```python
150
+ >>> fingerprint = "77c4b5b12b79887b8c709df826cff89a7268071593de5cc84053b8c00302b2fb"
151
+ >>> cg.get_issuer_fingerprint(fingerprint)
152
+ "965e8d87f96ccf564726ff5e97ea4d6b18731d9b762ef9510479f628bc85837e"
153
+ >>> cg.get_child_certificates(fingerprint)
154
+ ["3cf372288714b52298222bb1f8cf51dce3f1e60147ec53f9ee3dc28e8766c734", "2caf1b256bcbb327aea8d1442ba12e0fc6582b3c2980ee9f31d4ae3a62f62673"]
155
+ ```
156
+
157
+ Export graph to DOT language and write to a file, where it can be rendered with the `dot` command line tool.
158
+
159
+ ```python
160
+ >>> with open("path/to/output.dot", "w") as f:
161
+ >>> dot_str = cg.export_dot()
162
+ >>> f.write(dot_str)
163
+ ```
164
+
165
+ See more complex examples in the `examples` directory of the repository (**WIP**).
166
+
167
+ ## Documentation
168
+
169
+ **WIP**
@@ -8,6 +8,7 @@ src/certgraph.egg-info/SOURCES.txt
8
8
  src/certgraph.egg-info/dependency_links.txt
9
9
  src/certgraph.egg-info/requires.txt
10
10
  src/certgraph.egg-info/top_level.txt
11
+ tests/test_certificate_invalid_at.py
11
12
  tests/test_clear.py
12
13
  tests/test_export_dot.py
13
14
  tests/test_fingerprint_search.py
@@ -0,0 +1,39 @@
1
+ from datetime import timedelta
2
+
3
+ import pytest
4
+ from cryptography.hazmat.primitives import hashes
5
+
6
+
7
+ def test_raises_for_unknown_fingerprint(graph):
8
+ with pytest.raises(ValueError):
9
+ graph.certificate_invalid_at("does-not-exist")
10
+
11
+
12
+ def test_certificate_within_validity_period_is_valid(graph, self_signed_cert):
13
+ graph.import_certificates(self_signed_cert)
14
+ fingerprint = self_signed_cert.fingerprint(hashes.SHA256()).hex()
15
+
16
+ assert graph.certificate_invalid_at(fingerprint) is False
17
+
18
+
19
+ def test_expired_certificate_is_invalid(graph, expired_cert):
20
+ graph.import_certificates(expired_cert)
21
+ fingerprint = expired_cert.fingerprint(hashes.SHA256()).hex()
22
+
23
+ assert graph.certificate_invalid_at(fingerprint) is True
24
+
25
+
26
+ def test_not_yet_valid_certificate_is_invalid(graph, self_signed_cert):
27
+ graph.import_certificates(self_signed_cert)
28
+ fingerprint = self_signed_cert.fingerprint(hashes.SHA256()).hex()
29
+ before_validity = self_signed_cert.not_valid_before_utc - timedelta(days=1)
30
+
31
+ assert graph.certificate_invalid_at(fingerprint, at_time=before_validity) is True
32
+
33
+
34
+ def test_at_time_controls_validity_check(graph, self_signed_cert):
35
+ graph.import_certificates(self_signed_cert)
36
+ fingerprint = self_signed_cert.fingerprint(hashes.SHA256()).hex()
37
+ after_validity = self_signed_cert.not_valid_after_utc + timedelta(days=1)
38
+
39
+ assert graph.certificate_invalid_at(fingerprint, at_time=after_validity) is True
@@ -36,6 +36,23 @@ def test_export_dot_node_shape_is_box(graph, self_signed_cert):
36
36
  assert "shape=box" in dot
37
37
 
38
38
 
39
+ def test_export_dot_highlights_expired_certificate(graph, expired_cert):
40
+ graph.import_certificates(expired_cert)
41
+
42
+ dot = graph.export_dot()
43
+
44
+ assert "fillcolor=tomato" in dot
45
+
46
+
47
+ def test_export_dot_does_not_highlight_valid_certificate(graph, self_signed_cert):
48
+ graph.import_certificates(self_signed_cert)
49
+
50
+ dot = graph.export_dot()
51
+
52
+ assert "fillcolor=white" in dot
53
+ assert "tomato" not in dot
54
+
55
+
39
56
  def test_export_dot_includes_edges(graph, chain_certs):
40
57
  graph.import_certificates(chain_certs)
41
58
 
certgraph-0.3.0/PKG-INFO DELETED
@@ -1,42 +0,0 @@
1
- Metadata-Version: 2.4
2
- Name: certgraph
3
- Version: 0.3.0
4
- Summary: Utility for exploring and mapping X509 certificate chains.
5
- Author: Chris Adshead
6
- License: MIT License
7
-
8
- Copyright (c) 2026 Chris Adshead
9
-
10
- Permission is hereby granted, free of charge, to any person obtaining a copy
11
- of this software and associated documentation files (the "Software"), to deal
12
- in the Software without restriction, including without limitation the rights
13
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
14
- copies of the Software, and to permit persons to whom the Software is
15
- furnished to do so, subject to the following conditions:
16
-
17
- The above copyright notice and this permission notice shall be included in all
18
- copies or substantial portions of the Software.
19
-
20
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
21
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
22
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
23
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
24
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
25
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
26
- SOFTWARE.
27
-
28
- Requires-Python: >=3.12
29
- Description-Content-Type: text/markdown
30
- License-File: LICENSE
31
- Requires-Dist: cryptography
32
- Requires-Dist: networkx
33
- Requires-Dist: pydot
34
- Requires-Dist: rapidfuzz
35
- Provides-Extra: dev
36
- Requires-Dist: black; extra == "dev"
37
- Requires-Dist: pytest; extra == "dev"
38
- Requires-Dist: pytest-cov; extra == "dev"
39
- Dynamic: license-file
40
-
41
- # certgraph
42
- Python package for displaying X509 certificate chains.
certgraph-0.3.0/README.md DELETED
@@ -1,2 +0,0 @@
1
- # certgraph
2
- Python package for displaying X509 certificate chains.
@@ -1,42 +0,0 @@
1
- Metadata-Version: 2.4
2
- Name: certgraph
3
- Version: 0.3.0
4
- Summary: Utility for exploring and mapping X509 certificate chains.
5
- Author: Chris Adshead
6
- License: MIT License
7
-
8
- Copyright (c) 2026 Chris Adshead
9
-
10
- Permission is hereby granted, free of charge, to any person obtaining a copy
11
- of this software and associated documentation files (the "Software"), to deal
12
- in the Software without restriction, including without limitation the rights
13
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
14
- copies of the Software, and to permit persons to whom the Software is
15
- furnished to do so, subject to the following conditions:
16
-
17
- The above copyright notice and this permission notice shall be included in all
18
- copies or substantial portions of the Software.
19
-
20
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
21
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
22
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
23
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
24
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
25
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
26
- SOFTWARE.
27
-
28
- Requires-Python: >=3.12
29
- Description-Content-Type: text/markdown
30
- License-File: LICENSE
31
- Requires-Dist: cryptography
32
- Requires-Dist: networkx
33
- Requires-Dist: pydot
34
- Requires-Dist: rapidfuzz
35
- Provides-Extra: dev
36
- Requires-Dist: black; extra == "dev"
37
- Requires-Dist: pytest; extra == "dev"
38
- Requires-Dist: pytest-cov; extra == "dev"
39
- Dynamic: license-file
40
-
41
- # certgraph
42
- Python package for displaying X509 certificate chains.
File without changes
File without changes
File without changes
File without changes