Revize d28c02f7
Přidáno uživatelem Stanislav Král před asi 4 roky(ů)
openssl_poc.py | ||
---|---|---|
1 |
import subprocess |
|
2 |
import os |
|
3 |
import sys |
|
4 |
|
|
5 |
# directory where all certificates/keys will be stored |
|
6 |
WORK_DIR = "static/openssl" |
|
7 |
|
|
8 |
# name of the config file to be generated used when accepting a CSR |
|
9 |
ACCEPT_CSR_CONF_FILE = "accept_csr.cnf" |
|
10 |
|
|
11 |
# line to be inserted into the configuration file to be used when accepting a CA CSR |
|
12 |
CA_CNF_LINE = "basicConstraints=critical,CA:TRUE" |
|
13 |
|
|
14 |
|
|
15 |
# makes a private key without a passphrase (potentially insecure) |
|
16 |
def make_private_key(): |
|
17 |
return subprocess.check_output(["openssl", "genrsa", "2048"], stderr=subprocess.STDOUT) |
|
18 |
|
|
19 |
|
|
20 |
def make_root_ca(name, key, days): |
|
21 |
""" |
|
22 |
Creates a root CA |
|
23 |
|
|
24 |
:param name: name of the root CA to be used (will be passed to the Common Name field) |
|
25 |
:param key: private key of the CA to be used |
|
26 |
:param days: number of days for which the CA will be considered as valid |
|
27 |
:return: byte array containing the generated certificate |
|
28 |
""" |
|
29 |
return subprocess.check_output( |
|
30 |
["openssl", "req", "-new", "-x509", "-days", str(days), "-subj", f"/CN={name}", "-key", "-"], |
|
31 |
input=bytes(key, encoding="utf-8"), stderr=subprocess.STDOUT |
|
32 |
) |
|
33 |
|
|
34 |
|
|
35 |
def make_csr(key, name): |
|
36 |
""" |
|
37 |
Makes a CSR (Certificate Signing Request) |
|
38 |
:param key: the private key to be used to generate the certificate |
|
39 |
:param name: name of the certificate (will be passed to the Common Name field) |
|
40 |
:return: byte array containing the generated certificate signing request |
|
41 |
""" |
|
42 |
return subprocess.check_output( |
|
43 |
["openssl", "req", "-new", "-subj", f"/CN={name}", "-key", "-"], |
|
44 |
input=bytes(key, encoding="utf-8"), stderr=subprocess.STDOUT |
|
45 |
) |
|
46 |
|
|
47 |
|
|
48 |
def sign_csr(csr, ca_cert, ca_key, days, config=""): |
|
49 |
""" |
|
50 |
Signs the given CSR by the given CA |
|
51 |
:param csr: A string containing the CSR to be accepted |
|
52 |
:param ca_cert: A string containing the certificate of the issuer that will sign this CSR |
|
53 |
:param ca_key: A string containing the private key of the issuer's certificate |
|
54 |
:param days: The number of days for which the generated certificate will be considered as valid |
|
55 |
:param config: A string containing the configuration of extensions to be used |
|
56 |
:return: Byte array containing the generated and signed certificate |
|
57 |
""" |
|
58 |
|
|
59 |
# check whether any config has been passed |
|
60 |
config_used = len(config) > 0 |
|
61 |
config_file = get_path(ACCEPT_CSR_CONF_FILE) |
|
62 |
if config_used: |
|
63 |
# if config is to be applied the store it temporarily in a special file |
|
64 |
# that will be later passed to the openssl program |
|
65 |
puts(config, config_file) |
|
66 |
|
|
67 |
# prepare openssl parameters... |
|
68 |
# CSR, CA and CA's private key will be passed via stdin (that's the meaning of the '-' symbol) |
|
69 |
params = ["openssl", "x509", "-req", "-days", str(days), "-in", "-", "-CA", "-", "-CAkey", "-", "-CAcreateserial"] |
|
70 |
|
|
71 |
if config_used: |
|
72 |
# if config is to be applied add the generated configuration file path to the openssl program via parameters |
|
73 |
params.append("-extfile") |
|
74 |
params.append(config_file) |
|
75 |
|
|
76 |
# TODO delete any created files |
|
77 |
return subprocess.check_output(params, input=(bytes(csr + ca_cert + ca_key, encoding="utf-8")), |
|
78 |
stderr=subprocess.STDOUT) |
|
79 |
|
|
80 |
|
|
81 |
def get_aia_cnf_line(uri): |
|
82 |
""" |
|
83 |
Generates a line to be used in a configuration of X509 extensions that specifies that the current certificate will |
|
84 |
use Authority Information Access field to generate the chain of trust. |
|
85 |
:param uri: URI where the parent certificate lies |
|
86 |
:return: A line to be used in a configuration of X509 extensions |
|
87 |
""" |
|
88 |
return f"authorityInfoAccess = caIssuers;URI:{uri}" |
|
89 |
|
|
90 |
|
|
91 |
# returns a path of a file in a work directory |
|
92 |
def get_path(file): |
|
93 |
return f"{WORK_DIR}/{file}" |
|
94 |
|
|
95 |
|
|
96 |
# copies the given string to the file specified by a path |
|
97 |
def puts(content, file): |
|
98 |
with open(file, "w") as file: |
|
99 |
file.write(content) |
|
100 |
|
|
101 |
|
|
102 |
# reads all content of a file specified by a path |
|
103 |
def read_file(file): |
|
104 |
with open(file, "r") as file: |
|
105 |
return file.read() |
|
106 |
|
|
107 |
|
|
108 |
def setup_root_intermediate_cas(): |
|
109 |
""" |
|
110 |
Generates a root and an intermediate certificate authorities |
|
111 |
""" |
|
112 |
|
|
113 |
# generate a private key to be used to generate a root CA |
|
114 |
root_key = make_private_key().decode() |
|
115 |
# use the generated key to create a root CA |
|
116 |
root_ca = make_root_ca("ROOT CA", root_key, 1825).decode() |
|
117 |
|
|
118 |
# generate a private key to be used to generate an intermediate CA |
|
119 |
intermediate_key = make_private_key().decode() |
|
120 |
# use the generated key to make a CSR for a intermediate CA |
|
121 |
intermediate_csr = make_csr(intermediate_key, "INTER CA").decode() |
|
122 |
# sign the generated CSR by root CA |
|
123 |
# specify AIA field and that this certificate is a CA |
|
124 |
intermediate_ca = sign_csr(intermediate_csr, root_ca, root_key, 1096, |
|
125 |
f""" |
|
126 |
{get_aia_cnf_line("http://localhost:5000/static/openssl/root.crt")} |
|
127 |
{CA_CNF_LINE} |
|
128 |
""").decode() |
|
129 |
|
|
130 |
# store the generated root CA certificate/private key to files |
|
131 |
puts(root_ca, get_path("root.crt")) |
|
132 |
puts(root_key, get_path("root.key")) |
|
133 |
|
|
134 |
# store the generated intermediate CA certificate/private key to files |
|
135 |
puts(intermediate_ca, get_path("inter.crt")) |
|
136 |
puts(intermediate_key, get_path("inter.key")) |
|
137 |
|
|
138 |
# return the generated intermediate CA certificate |
|
139 |
return intermediate_ca, intermediate_key |
|
140 |
|
|
141 |
|
|
142 |
# USAGE (create a chain of trust that consists of root and inter. CAs and a generated certificate signed by int. CA): |
|
143 |
# |
|
144 |
# "python3 openssl_poc.py MyTestCertificate" |
|
145 |
# |
|
146 |
# the generated certificate will have the Common Name field set to "MyTestCertificate" |
|
147 |
# and will be stored to MyTestCertificate.crt |
|
148 |
if __name__ == "__main__": |
|
149 |
# a name of the child certificate to be generated must be passed to the program via arguments |
|
150 |
if len(sys.argv) > 1: |
|
151 |
cert_name = sys.argv[1] |
|
152 |
|
|
153 |
# ensure that WORK_DIR directory exists |
|
154 |
try: |
|
155 |
os.mkdir(WORK_DIR) |
|
156 |
except IOError: |
|
157 |
pass |
|
158 |
|
|
159 |
# the program will generate a child certificate that is signed by an intermediate CA |
|
160 |
|
|
161 |
# root CA certificate, intermediate CA certificate and the generated user specified child certificate will be |
|
162 |
# save to static/openssl directory |
|
163 |
|
|
164 |
# if any of the files required to create such certificate is missing then the program will regenerate the whole |
|
165 |
# chain of trust (root/inter. CA cert/key) |
|
166 |
|
|
167 |
try: |
|
168 |
# try to read all required files (exception is thrown if any of the files are non-existent) |
|
169 |
read_file(get_path("root.crt")) |
|
170 |
inter_ca = read_file(get_path("inter.crt")) |
|
171 |
inter_key = read_file(get_path("inter.key")) |
|
172 |
except (IOError, FileNotFoundError): |
|
173 |
print("Creating ROOT and INTERMEDIATE CAs...") |
|
174 |
# any of the required files is missing - generate new ones |
|
175 |
(inter_ca, inter_key) = setup_root_intermediate_cas() |
|
176 |
|
|
177 |
# make a private key to be used for generating a child certificate |
|
178 |
child_key = make_private_key().decode() |
|
179 |
|
|
180 |
# generate Common Name field to be specified in the generated child certificate |
|
181 |
child_cert_name = f"{cert_name} - CERT" |
|
182 |
|
|
183 |
# generate a CSR to sign the child certificate |
|
184 |
child_csr = make_csr(child_key, child_cert_name).decode() |
|
185 |
|
|
186 |
# sign the CSR by the intermediate CA (make sure inter. cert. is available via AIA) |
|
187 |
child_ca = sign_csr(child_csr, inter_ca, inter_key, 1096, |
|
188 |
f""" |
|
189 |
{get_aia_cnf_line("http://localhost:5000/static/openssl/inter.crt")} |
|
190 |
""").decode() |
|
191 |
|
|
192 |
# generate a path of a file in which the child cert. will be stored |
|
193 |
child_ca_file = get_path(f"{cert_name}.crt") |
|
194 |
# store the generated child cert. |
|
195 |
puts(child_ca, child_ca_file) |
|
196 |
|
|
197 |
# store the generated private key used to generate the child. cert |
|
198 |
puts(child_key, get_path(f"{cert_name}.key")) |
|
199 |
|
|
200 |
##################################### |
|
201 |
# TEST SECTION |
|
202 |
|
|
203 |
# read content of the root CA certificate |
|
204 |
root_cert_content = subprocess.check_output(["openssl", "x509", "-noout", "-text", "-in", "-"], |
|
205 |
input=bytes(read_file(get_path("root.crt")), |
|
206 |
encoding="utf-8")).decode() |
|
207 |
|
|
208 |
# assert the common name of the certificate and that the certificate represents a CA |
|
209 |
assert f"Subject: CN = ROOT CA" in root_cert_content |
|
210 |
assert "CA:TRUE" in root_cert_content |
|
211 |
|
|
212 |
# read content of the intermediate CA certificate |
|
213 |
inter_cert_content = subprocess.check_output(["openssl", "x509", "-noout", "-text", "-in", "-"], |
|
214 |
input=bytes(inter_ca, encoding="utf-8")).decode() |
|
215 |
|
|
216 |
# assert that the issuer of the certificate is the root CA |
|
217 |
assert "Issuer: CN = ROOT CA" in inter_cert_content |
|
218 |
# assert the common name |
|
219 |
assert f"Subject: CN = INTER CA" in inter_cert_content |
|
220 |
# assert that the certificate contains a reference to the root CA's certificate via AIA |
|
221 |
assert "CA Issuers - URI:http://localhost:5000/static/openssl/root.crt" in inter_cert_content |
|
222 |
# ensure that the generated certificate is indeed a CA |
|
223 |
assert "CA:TRUE" in inter_cert_content |
|
224 |
|
|
225 |
# read content of the child certificate |
|
226 |
child_cert_content = subprocess.check_output( |
|
227 |
["openssl", "x509", "-noout", "-text", "-in", child_ca_file]).decode() |
|
228 |
|
|
229 |
# assert that the issuer is the intermediate CA |
|
230 |
assert "Issuer: CN = INTER CA" in child_cert_content |
|
231 |
# assert that the common name is the one passed via arguments |
|
232 |
assert f"Subject: CN = {child_cert_name}" in child_cert_content |
|
233 |
# assert that the certificate contains a reference to the intermediate CA's certificate via AIA |
|
234 |
assert "CA Issuers - URI:http://localhost:5000/static/openssl/inter.crt" in child_cert_content |
|
235 |
# assert that the generated certificate is not a CA |
|
236 |
assert "CA:TRUE" not in child_cert_content |
|
237 |
|
|
238 |
# assert that the chain of trust is valid (root -> intermediate -> child) |
|
239 |
assert subprocess.check_output( |
|
240 |
["openssl", "verify", "-CAfile", get_path("root.crt"), "-untrusted", get_path("inter.crt"), child_ca_file]) |
|
241 |
|
|
242 |
print(f"TESTS PASSED!!! \n{cert_name} certificate created successfully") |
Také k dispozici: Unified diff
Re #8307 - added a proof of concept that demonstrates the ability to create a chain of trust
- a Python script was added that is able to generate a chain of trust consisting of root and intermediate CAs. In addition to that a child certificate with the specified common name will be generated and signed by the intermediate CA.
- the validity of the chain is ensured by a number of assert calls