certgraph 0.2.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.
- certgraph-0.4.0/PKG-INFO +169 -0
- certgraph-0.4.0/README.md +129 -0
- {certgraph-0.2.0 → certgraph-0.4.0}/pyproject.toml +1 -1
- {certgraph-0.2.0 → certgraph-0.4.0}/src/certgraph/certgraph.py +64 -18
- certgraph-0.4.0/src/certgraph.egg-info/PKG-INFO +169 -0
- {certgraph-0.2.0 → certgraph-0.4.0}/src/certgraph.egg-info/SOURCES.txt +1 -0
- certgraph-0.4.0/tests/test_certificate_invalid_at.py +39 -0
- certgraph-0.4.0/tests/test_export_dot.py +71 -0
- certgraph-0.2.0/PKG-INFO +0 -42
- certgraph-0.2.0/README.md +0 -2
- certgraph-0.2.0/src/certgraph.egg-info/PKG-INFO +0 -42
- certgraph-0.2.0/tests/test_export_dot.py +0 -9
- {certgraph-0.2.0 → certgraph-0.4.0}/LICENSE +0 -0
- {certgraph-0.2.0 → certgraph-0.4.0}/setup.cfg +0 -0
- {certgraph-0.2.0 → certgraph-0.4.0}/src/certgraph/__init__.py +0 -0
- {certgraph-0.2.0 → certgraph-0.4.0}/src/certgraph.egg-info/dependency_links.txt +0 -0
- {certgraph-0.2.0 → certgraph-0.4.0}/src/certgraph.egg-info/requires.txt +0 -0
- {certgraph-0.2.0 → certgraph-0.4.0}/src/certgraph.egg-info/top_level.txt +0 -0
- {certgraph-0.2.0 → certgraph-0.4.0}/tests/test_clear.py +0 -0
- {certgraph-0.2.0 → certgraph-0.4.0}/tests/test_fingerprint_search.py +0 -0
- {certgraph-0.2.0 → certgraph-0.4.0}/tests/test_get_certificate.py +0 -0
- {certgraph-0.2.0 → certgraph-0.4.0}/tests/test_get_child_fingerprints.py +0 -0
- {certgraph-0.2.0 → certgraph-0.4.0}/tests/test_get_issuer_fingerprint.py +0 -0
- {certgraph-0.2.0 → certgraph-0.4.0}/tests/test_graph.py +0 -0
- {certgraph-0.2.0 → certgraph-0.4.0}/tests/test_import_certificates.py +0 -0
- {certgraph-0.2.0 → certgraph-0.4.0}/tests/test_remove_certificate.py +0 -0
certgraph-0.4.0/PKG-INFO
ADDED
|
@@ -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
|
+
[](https://pypi.org/project/certgraph/)
|
|
44
|
+
[](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
|
+

|
|
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
|
+
[](https://pypi.org/project/certgraph/)
|
|
4
|
+
[](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
|
+

|
|
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**
|
|
@@ -1,17 +1,22 @@
|
|
|
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
|
|
|
6
7
|
|
|
7
8
|
class certgraph:
|
|
8
|
-
"""Class for creating and exploring directed graphs of
|
|
9
|
+
"""Class for creating and exploring directed graphs of X.509 certificates."""
|
|
9
10
|
|
|
10
11
|
def __init__(self) -> None:
|
|
11
12
|
"""Initialise an empty certgraph object."""
|
|
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: (
|
|
@@ -88,25 +93,39 @@ class certgraph:
|
|
|
88
93
|
"""
|
|
89
94
|
return [f"{edge[0][:8]} -> {edge[1][:8]}" for edge in self._graph.edges()]
|
|
90
95
|
|
|
91
|
-
def export_dot(self
|
|
92
|
-
"""
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
if format not in allowed_types:
|
|
96
|
-
raise ValueError(
|
|
97
|
-
f"Cannot export dot graph of type {format} - must be one of {allowed_types}"
|
|
98
|
-
)
|
|
96
|
+
def export_dot(self) -> str:
|
|
97
|
+
"""
|
|
98
|
+
Export the current certificate digraph in the dot language.
|
|
99
99
|
|
|
100
|
+
Returns:
|
|
101
|
+
The certificate digraph rendered as a dot language string.
|
|
102
|
+
"""
|
|
100
103
|
dot_graph = nx.DiGraph()
|
|
101
104
|
|
|
102
105
|
for fingerprint, data in self._graph.nodes(data=True):
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
+
cert: x509.Certificate = data["certificate"]
|
|
107
|
+
validity_time_str = f"{cert.not_valid_before_utc.isoformat()} → {cert.not_valid_after_utc.isoformat()}"
|
|
108
|
+
invalid = self.certificate_invalid_at(fingerprint)
|
|
106
109
|
|
|
107
|
-
|
|
110
|
+
label = f"DN: {cert.subject.rfc4514_string()}"
|
|
111
|
+
label += f"\nValid: {validity_time_str}"
|
|
108
112
|
|
|
109
|
-
|
|
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
|
+
)
|
|
125
|
+
|
|
126
|
+
dot_graph.add_edges_from(self._graph.edges())
|
|
127
|
+
|
|
128
|
+
return nx.drawing.nx_pydot.to_pydot(dot_graph).to_string()
|
|
110
129
|
|
|
111
130
|
def clear(self) -> certgraph:
|
|
112
131
|
"""
|
|
@@ -170,7 +189,7 @@ class certgraph:
|
|
|
170
189
|
|
|
171
190
|
Args:
|
|
172
191
|
fingerprint: Fingerprint of the certificate to get the issuer of.
|
|
173
|
-
|
|
192
|
+
|
|
174
193
|
Returns:
|
|
175
194
|
The fingerprint of the issuing certificate or None if not present in the digraph.
|
|
176
195
|
|
|
@@ -178,7 +197,9 @@ class certgraph:
|
|
|
178
197
|
ValueError: If certificate with the supplied fingerprint isn't in the digraph.
|
|
179
198
|
"""
|
|
180
199
|
if fingerprint not in self._graph.nodes:
|
|
181
|
-
raise ValueError(
|
|
200
|
+
raise ValueError(
|
|
201
|
+
f"Fingerprint supplied doesn't match any imported certificate."
|
|
202
|
+
)
|
|
182
203
|
|
|
183
204
|
return next(self._graph.predecessors(fingerprint), None)
|
|
184
205
|
|
|
@@ -188,7 +209,7 @@ class certgraph:
|
|
|
188
209
|
|
|
189
210
|
Args:
|
|
190
211
|
fingerprint: Fingerprint of the certificate to get the child certificates of.
|
|
191
|
-
|
|
212
|
+
|
|
192
213
|
Returns:
|
|
193
214
|
The list of fingerprints belonging to child certificates.
|
|
194
215
|
|
|
@@ -196,7 +217,9 @@ class certgraph:
|
|
|
196
217
|
ValueError: If certificate with the supplied fingerprint isn't in the digraph.
|
|
197
218
|
"""
|
|
198
219
|
if fingerprint not in self._graph.nodes:
|
|
199
|
-
raise ValueError(
|
|
220
|
+
raise ValueError(
|
|
221
|
+
f"Fingerprint supplied doesn't match any imported certificate."
|
|
222
|
+
)
|
|
200
223
|
|
|
201
224
|
return list(self._graph.successors(fingerprint))
|
|
202
225
|
|
|
@@ -215,3 +238,26 @@ class certgraph:
|
|
|
215
238
|
if cert in self._certlist:
|
|
216
239
|
self._certlist.remove(cert)
|
|
217
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
|
+
[](https://pypi.org/project/certgraph/)
|
|
44
|
+
[](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
|
+

|
|
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
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
def test_export_dot_returns_string(graph):
|
|
2
|
+
assert isinstance(graph.export_dot(), str)
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def test_export_dot_empty_graph_has_no_edges(graph):
|
|
6
|
+
dot = graph.export_dot()
|
|
7
|
+
assert "digraph" in dot
|
|
8
|
+
assert "->" not in dot
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def test_export_dot_includes_node_labels(graph, self_signed_cert):
|
|
12
|
+
graph.import_certificates(self_signed_cert)
|
|
13
|
+
dn = self_signed_cert.subject.rfc4514_string()
|
|
14
|
+
|
|
15
|
+
dot = graph.export_dot()
|
|
16
|
+
|
|
17
|
+
assert f"DN: {dn}" in dot
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def test_export_dot_includes_validity_period(graph, self_signed_cert):
|
|
21
|
+
graph.import_certificates(self_signed_cert)
|
|
22
|
+
not_before = self_signed_cert.not_valid_before_utc.isoformat()
|
|
23
|
+
not_after = self_signed_cert.not_valid_after_utc.isoformat()
|
|
24
|
+
|
|
25
|
+
dot = graph.export_dot()
|
|
26
|
+
|
|
27
|
+
# joined with an arrow character rather than '->', to avoid being confused with edge syntax
|
|
28
|
+
assert f"Valid: {not_before} → {not_after}" in dot
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def test_export_dot_node_shape_is_box(graph, self_signed_cert):
|
|
32
|
+
graph.import_certificates(self_signed_cert)
|
|
33
|
+
|
|
34
|
+
dot = graph.export_dot()
|
|
35
|
+
|
|
36
|
+
assert "shape=box" in dot
|
|
37
|
+
|
|
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
|
+
|
|
56
|
+
def test_export_dot_includes_edges(graph, chain_certs):
|
|
57
|
+
graph.import_certificates(chain_certs)
|
|
58
|
+
|
|
59
|
+
dot = graph.export_dot()
|
|
60
|
+
|
|
61
|
+
# one self-loop for the root, plus one edge per subsequent certificate
|
|
62
|
+
assert dot.count("->") == len(chain_certs)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def test_export_dot_does_not_write_file(graph, self_signed_cert, tmp_path, monkeypatch):
|
|
66
|
+
monkeypatch.chdir(tmp_path)
|
|
67
|
+
graph.import_certificates(self_signed_cert)
|
|
68
|
+
|
|
69
|
+
graph.export_dot()
|
|
70
|
+
|
|
71
|
+
assert not (tmp_path / "test_out.dot").exists()
|
certgraph-0.2.0/PKG-INFO
DELETED
|
@@ -1,42 +0,0 @@
|
|
|
1
|
-
Metadata-Version: 2.4
|
|
2
|
-
Name: certgraph
|
|
3
|
-
Version: 0.2.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.2.0/README.md
DELETED
|
@@ -1,42 +0,0 @@
|
|
|
1
|
-
Metadata-Version: 2.4
|
|
2
|
-
Name: certgraph
|
|
3
|
-
Version: 0.2.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.
|
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
import pytest
|
|
2
|
-
|
|
3
|
-
# export_dot()'s actual output generation is still rough and not covered here.
|
|
4
|
-
# Only the argument validation, which happens before anything is written, is tested.
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
def test_export_dot_rejects_invalid_format(graph):
|
|
8
|
-
with pytest.raises(ValueError):
|
|
9
|
-
graph.export_dot(format="jpeg")
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|