1 |
d28c02f7
|
Stanislav Král
|
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")
|