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
|
# PFX store containing the whole chain of trust will also be generated (can be imported to Adobe Acrobat Reader)
|
149
|
if __name__ == "__main__":
|
150
|
# a name of the child certificate to be generated must be passed to the program via arguments
|
151
|
if len(sys.argv) > 1:
|
152
|
cert_name = sys.argv[1]
|
153
|
|
154
|
# ensure that WORK_DIR directory exists
|
155
|
try:
|
156
|
os.mkdir(WORK_DIR)
|
157
|
except IOError:
|
158
|
pass
|
159
|
|
160
|
# the program will generate a child certificate that is signed by an intermediate CA
|
161
|
|
162
|
# root CA certificate, intermediate CA certificate and the generated user specified child certificate will be
|
163
|
# save to static/openssl directory
|
164
|
|
165
|
# if any of the files required to create such certificate is missing then the program will regenerate the whole
|
166
|
# chain of trust (root/inter. CA cert/key)
|
167
|
|
168
|
try:
|
169
|
# try to read all required files (exception is thrown if any of the files are non-existent)
|
170
|
read_file(get_path("root.crt"))
|
171
|
inter_ca = read_file(get_path("inter.crt"))
|
172
|
inter_key = read_file(get_path("inter.key"))
|
173
|
except (IOError, FileNotFoundError):
|
174
|
print("Creating ROOT and INTERMEDIATE CAs...")
|
175
|
# any of the required files is missing - generate new ones
|
176
|
(inter_ca, inter_key) = setup_root_intermediate_cas()
|
177
|
|
178
|
# make a private key to be used for generating a child certificate
|
179
|
child_key = make_private_key().decode()
|
180
|
|
181
|
# generate Common Name field to be specified in the generated child certificate
|
182
|
child_cert_name = f"{cert_name} - CERT"
|
183
|
|
184
|
# generate a CSR to sign the child certificate
|
185
|
child_csr = make_csr(child_key, child_cert_name).decode()
|
186
|
|
187
|
# sign the CSR by the intermediate CA (make sure inter. cert. is available via AIA)
|
188
|
child_ca = sign_csr(child_csr, inter_ca, inter_key, 1096,
|
189
|
f"""
|
190
|
{get_aia_cnf_line("http://localhost:5000/static/openssl/inter.crt")}
|
191
|
""").decode()
|
192
|
|
193
|
# generate a path of a file in which the child cert. will be stored
|
194
|
child_ca_file = get_path(f"{cert_name}.crt")
|
195
|
# store the generated child cert.
|
196
|
puts(child_ca, child_ca_file)
|
197
|
|
198
|
# store the generated private key used to generate the child. cert
|
199
|
child_key_file = get_path(f"{cert_name}.key")
|
200
|
puts(child_key, child_key_file)
|
201
|
|
202
|
# export the whole chain of trust including the generated child certificate to a PKCS store
|
203
|
# "pass" passphrase will be used to encrypt the store
|
204
|
subprocess.check_output(
|
205
|
["openssl", "pkcs12", "-export", "-out", f"{get_path(cert_name)}.pfx", "-inkey", child_key_file, "-in",
|
206
|
child_ca_file, "-certfile", get_path("inter.crt"), "-passout", "pass:pass"],
|
207
|
encoding="utf-8")
|
208
|
|
209
|
#####################################
|
210
|
# TEST SECTION
|
211
|
|
212
|
# read content of the root CA certificate
|
213
|
root_cert_content = subprocess.check_output(["openssl", "x509", "-noout", "-text", "-in", "-"],
|
214
|
input=bytes(read_file(get_path("root.crt")),
|
215
|
encoding="utf-8")).decode()
|
216
|
|
217
|
# assert the common name of the certificate and that the certificate represents a CA
|
218
|
assert f"Subject: CN = ROOT CA" in root_cert_content
|
219
|
assert "CA:TRUE" in root_cert_content
|
220
|
|
221
|
# read content of the intermediate CA certificate
|
222
|
inter_cert_content = subprocess.check_output(["openssl", "x509", "-noout", "-text", "-in", "-"],
|
223
|
input=bytes(inter_ca, encoding="utf-8")).decode()
|
224
|
|
225
|
# assert that the issuer of the certificate is the root CA
|
226
|
assert "Issuer: CN = ROOT CA" in inter_cert_content
|
227
|
# assert the common name
|
228
|
assert f"Subject: CN = INTER CA" in inter_cert_content
|
229
|
# assert that the certificate contains a reference to the root CA's certificate via AIA
|
230
|
assert "CA Issuers - URI:http://localhost:5000/static/openssl/root.crt" in inter_cert_content
|
231
|
# ensure that the generated certificate is indeed a CA
|
232
|
assert "CA:TRUE" in inter_cert_content
|
233
|
|
234
|
# read content of the child certificate
|
235
|
child_cert_content = subprocess.check_output(
|
236
|
["openssl", "x509", "-noout", "-text", "-in", child_ca_file]).decode()
|
237
|
|
238
|
# assert that the issuer is the intermediate CA
|
239
|
assert "Issuer: CN = INTER CA" in child_cert_content
|
240
|
# assert that the common name is the one passed via arguments
|
241
|
assert f"Subject: CN = {child_cert_name}" in child_cert_content
|
242
|
# assert that the certificate contains a reference to the intermediate CA's certificate via AIA
|
243
|
assert "CA Issuers - URI:http://localhost:5000/static/openssl/inter.crt" in child_cert_content
|
244
|
# assert that the generated certificate is not a CA
|
245
|
assert "CA:TRUE" not in child_cert_content
|
246
|
|
247
|
# assert that the chain of trust is valid (root -> intermediate -> child)
|
248
|
assert subprocess.check_output(
|
249
|
["openssl", "verify", "-CAfile", get_path("root.crt"), "-untrusted", get_path("inter.crt"), child_ca_file])
|
250
|
|
251
|
print(f"""TESTS PASSED!!! \n\n"{cert_name}" certificate created successfully""")
|
252
|
print(
|
253
|
f"""PFX store with the generated certificate and the whole chain of trust saved to:\n{cert_name}.pfx (protecteed by "pass" passphrase)""")
|