blob: 47e3592f8e2cd0c8f3d586d09ccc07ac1a2d3362 [file] [log] [blame]
Alex Schendel47af8322023-07-21 13:32:38 -07001#!/usr/bin/env python3
2
3import argparse
4import os
Ed Tanous7b9e2562024-04-07 20:24:12 -07005import socket
6import urllib
Alex Schendel47af8322023-07-21 13:32:38 -07007
8import requests
9
10try:
11 import redfish
12except ModuleNotFoundError:
13 raise Exception("Please run pip install redfish to run this script.")
14try:
15 from OpenSSL import crypto
16except ImportError:
17 raise Exception("Please run pip install pyOpenSSL to run this script.")
18
Ed Tanous7b9e2562024-04-07 20:24:12 -070019SCRIPT_DIR = os.path.dirname(os.path.realpath(__file__))
20
Alex Schendel47af8322023-07-21 13:32:38 -070021# Script to generate a certificates for a CA, server, and client
22# allowing for client authentication using mTLS certificates.
23# This can then be used to test mTLS client authentication for Redfish
24# and webUI. Note that this requires the pyOpenSSL library to function.
25# TODO: Use EC keys rather than RSA keys.
26
27
28def generateCACert(serial):
29 # CA key
30 key = crypto.PKey()
31 key.generate_key(crypto.TYPE_RSA, 2048)
32
33 # CA cert
34 cert = crypto.X509()
35 cert.set_serial_number(serial)
36 cert.set_version(2)
37 cert.set_pubkey(key)
Ed Tanous7b9e2562024-04-07 20:24:12 -070038
39 cert.set_notBefore(b"19700101000000Z")
40 cert.set_notAfter(b"20700101000000Z")
Alex Schendel47af8322023-07-21 13:32:38 -070041
42 caCertSubject = cert.get_subject()
43 caCertSubject.countryName = "US"
44 caCertSubject.stateOrProvinceName = "California"
45 caCertSubject.localityName = "San Francisco"
46 caCertSubject.organizationName = "OpenBMC"
47 caCertSubject.organizationalUnitName = "bmcweb"
48 caCertSubject.commonName = "Test CA"
49 cert.set_issuer(caCertSubject)
50
51 cert.add_extensions(
52 [
53 crypto.X509Extension(
54 b"basicConstraints", True, b"CA:TRUE, pathlen:0"
55 ),
56 crypto.X509Extension(b"keyUsage", True, b"keyCertSign, cRLSign"),
57 crypto.X509Extension(
58 b"subjectKeyIdentifier", False, b"hash", subject=cert
59 ),
60 ]
61 )
62 cert.add_extensions(
63 [
64 crypto.X509Extension(
65 b"authorityKeyIdentifier", False, b"keyid:always", issuer=cert
Ed Tanous7b9e2562024-04-07 20:24:12 -070066 ),
Alex Schendel47af8322023-07-21 13:32:38 -070067 ]
68 )
69
70 # sign CA cert with CA key
71 cert.sign(key, "sha256")
72 return key, cert
73
74
Ed Tanous7b9e2562024-04-07 20:24:12 -070075def generateCertCsr(
76 redfishObject, commonName, extensions, caKey, caCert, serial
77):
78
79 try:
80 socket.inet_aton(commonName)
81 commonName = "IP: " + commonName
82 except socket.error:
83 commonName = "DNS: " + commonName
84
85 CSRRequest = {
86 "CommonName": commonName,
87 "City": "San Fransisco",
88 "Country": "US",
89 "Organization": "",
90 "OrganizationalUnit": "",
91 "State": "CA",
92 "CertificateCollection": {
93 "@odata.id": "/redfish/v1/Managers/bmc/NetworkProtocol/HTTPS/Certificates",
94 },
95 "AlternativeNames": [
96 commonName,
97 "DNS: localhost",
98 "IP: 127.0.0.1",
99 ],
100 }
101
102 response = redfishObject.post(
103 "/redfish/v1/CertificateService/Actions/CertificateService.GenerateCSR",
104 body=CSRRequest,
105 )
106
107 if response.status != 200:
108 raise Exception("Failed to create CSR")
109
110 csrString = response.dict["CSRString"]
111
112 return crypto.load_certificate_request(crypto.FILETYPE_PEM, csrString)
113
114
115def generateCert(commonName, extensions, caKey, caCert, serial, csr=None):
Alex Schendel47af8322023-07-21 13:32:38 -0700116 # key
Alex Schendel47af8322023-07-21 13:32:38 -0700117
118 # cert
119 cert = crypto.X509()
Alex Schendel47af8322023-07-21 13:32:38 -0700120 cert.set_serial_number(serial)
121 cert.set_version(2)
Ed Tanous7b9e2562024-04-07 20:24:12 -0700122
123 if csr is None:
124 key = crypto.PKey()
125 key.generate_key(crypto.TYPE_RSA, 2048)
126 cert.set_pubkey(key)
127 else:
128 key = None
129 cert.set_subject(csr.get_subject())
130 cert.set_pubkey(csr.get_pubkey())
131
132 cert.set_notBefore(b"19700101000000Z")
133 cert.set_notAfter(b"20700101000000Z")
Alex Schendel47af8322023-07-21 13:32:38 -0700134
135 certSubject = cert.get_subject()
136 certSubject.countryName = "US"
137 certSubject.stateOrProvinceName = "California"
138 certSubject.localityName = "San Francisco"
139 certSubject.organizationName = "OpenBMC"
140 certSubject.organizationalUnitName = "bmcweb"
141 certSubject.commonName = commonName
142 cert.set_issuer(caCert.get_issuer())
143
Ed Tanous7b9e2562024-04-07 20:24:12 -0700144 extensions.extend(
Alex Schendel47af8322023-07-21 13:32:38 -0700145 [
146 crypto.X509Extension(
147 b"authorityKeyIdentifier", False, b"keyid", issuer=caCert
Ed Tanous7b9e2562024-04-07 20:24:12 -0700148 ),
Alex Schendel47af8322023-07-21 13:32:38 -0700149 ]
150 )
Ed Tanous7b9e2562024-04-07 20:24:12 -0700151 cert.add_extensions(extensions)
Alex Schendel47af8322023-07-21 13:32:38 -0700152 cert.sign(caKey, "sha256")
153 return key, cert
154
155
156def main():
157 parser = argparse.ArgumentParser()
158 parser.add_argument("--host", help="Host to connect to", required=True)
159 parser.add_argument(
160 "--username", help="Username to connect with", default="root"
161 )
162 parser.add_argument(
163 "--password",
164 help="Password for user in order to install certs over Redfish.",
165 default="0penBmc",
166 )
167 args = parser.parse_args()
168 host = args.host
169 username = args.username
170 password = args.password
171 if username == "root" and password == "0penBMC":
172 print(
173 """Note: Using default username 'root' and default password
174 '0penBmc'. Use --username and --password flags to change these,
175 respectively."""
176 )
Ed Tanous7b9e2562024-04-07 20:24:12 -0700177 if "//" not in host:
178 host = f"https://{host}"
179 url = urllib.parse.urlparse(host, scheme="https")
Alex Schendel47af8322023-07-21 13:32:38 -0700180
Ed Tanous7b9e2562024-04-07 20:24:12 -0700181 serial = 1000
182 certsDir = os.path.join(SCRIPT_DIR, "certs")
183 print(f"Writing certs to {certsDir}")
Alex Schendel47af8322023-07-21 13:32:38 -0700184 try:
185 print("Making certs directory.")
Ed Tanous7b9e2562024-04-07 20:24:12 -0700186 os.mkdir(certsDir)
Alex Schendel47af8322023-07-21 13:32:38 -0700187 except OSError as error:
188 if error.errno == 17:
189 print("certs directory already exists. Skipping...")
190 else:
191 print(error)
Ed Tanous7b9e2562024-04-07 20:24:12 -0700192
193 cacertFilename = os.path.join(certsDir, "CA-cert.cer")
194 cakeyFilename = os.path.join(certsDir, "CA-key.pem")
195 if os.path.exists(cacertFilename):
196 with open(cacertFilename, "rb") as cacert_file:
197 caCertDump = cacert_file.read()
198 caCert = crypto.load_certificate(crypto.FILETYPE_PEM, caCertDump)
199 with open(cakeyFilename, "rb") as cakey_file:
200 caKeyDump = cakey_file.read()
201 caKey = crypto.load_privatekey(crypto.FILETYPE_PEM, caKeyDump)
202 else:
203
204 caKey, caCert = generateCACert(serial)
205 caKeyDump = crypto.dump_privatekey(crypto.FILETYPE_PEM, caKey)
206 caCertDump = crypto.dump_certificate(crypto.FILETYPE_PEM, caCert)
207 with open(cacertFilename, "wb") as f:
208 f.write(caCertDump)
209 print("CA cert generated.")
210 with open(cakeyFilename, "wb") as f:
211 f.write(caKeyDump)
212 print("CA key generated.")
Alex Schendel47af8322023-07-21 13:32:38 -0700213 serial += 1
Alex Schendel47af8322023-07-21 13:32:38 -0700214
215 clientExtensions = [
216 crypto.X509Extension(
217 b"keyUsage",
218 True,
219 b"""digitalSignature,
220 keyAgreement""",
221 ),
222 crypto.X509Extension(b"extendedKeyUsage", True, b"clientAuth"),
223 ]
Alex Schendel47af8322023-07-21 13:32:38 -0700224
Alex Schendel47af8322023-07-21 13:32:38 -0700225 redfishObject = redfish.redfish_client(
Ed Tanous7b9e2562024-04-07 20:24:12 -0700226 base_url="https://" + url.netloc,
Alex Schendel47af8322023-07-21 13:32:38 -0700227 username=username,
228 password=password,
229 default_prefix="/redfish/v1",
230 )
231 redfishObject.login(auth="session")
Ed Tanous7b9e2562024-04-07 20:24:12 -0700232
233 clientKey, clientCert = generateCert(
234 username, clientExtensions, caKey, caCert, serial
235 )
236
237 clientKeyDump = crypto.dump_privatekey(crypto.FILETYPE_PEM, clientKey)
238 with open(os.path.join(certsDir, "client-key.pem"), "wb") as f:
239 f.write(clientKeyDump)
240 print("Client key generated.")
241 serial += 1
242 clientCertDump = crypto.dump_certificate(crypto.FILETYPE_PEM, clientCert)
243
244 with open(os.path.join(certsDir, "client-cert.pem"), "wb") as f:
245 f.write(clientCertDump)
246 print("Client cert generated.")
247
248 san_list = [
249 b"DNS: localhost",
250 b"IP: 127.0.0.1",
251 ]
252
253 try:
254 socket.inet_aton(url.hostname)
255 san_list.append(b"IP: " + url.hostname.encode())
256 except socket.error:
257 san_list.append(b"DNS: " + url.hostname.encode())
258
259 serverExtensions = [
260 crypto.X509Extension(
261 b"keyUsage",
262 True,
263 b"digitalSignature, keyAgreement",
264 ),
265 crypto.X509Extension(b"extendedKeyUsage", True, b"serverAuth"),
266 crypto.X509Extension(b"subjectAltName", False, b", ".join(san_list)),
267 ]
268
269 useCSR = True
270
271 if useCSR:
272 csr = generateCertCsr(
273 redfishObject,
274 url.hostname,
275 serverExtensions,
276 caKey,
277 caCert,
278 serial,
279 )
280 serverKey = None
281 serverKeyDumpStr = ""
282 else:
283 csr = None
284 serverKey, serverCert = generateCert(
285 url.hostname, serverExtensions, caKey, caCert, serial, csr=csr
286 )
287 if serverKey is not None:
288 serverKeyDump = crypto.dump_privatekey(crypto.FILETYPE_PEM, serverKey)
289 with open(os.path.join(certsDir, "server-key.pem"), "wb") as f:
290 f.write(serverKeyDump)
291 print("Server key generated.")
292 serverKeyDumpStr = serverKeyDump.decode()
293 serial += 1
294
295 serverCertDump = crypto.dump_certificate(crypto.FILETYPE_PEM, serverCert)
296
297 with open(os.path.join(certsDir, "server-cert.pem"), "wb") as f:
298 f.write(serverCertDump)
299 print("Server cert generated.")
300
301 serverCertDumpStr = serverCertDump.decode()
302
303 print("Generating p12 cert file for browser authentication.")
304 pkcs12Cert = crypto.PKCS12()
305 pkcs12Cert.set_certificate(clientCert)
306 if clientKey:
307 pkcs12Cert.set_privatekey(clientKey)
308 pkcs12Cert.set_ca_certificates([caCert])
309 pkcs12Cert.set_friendlyname(bytes(username, encoding="utf-8"))
310 with open(os.path.join(certsDir, "client.p12"), "wb") as f:
311 f.write(pkcs12Cert.export())
312 print("Client p12 cert file generated and stored in client.p12.")
313 print(
314 "Copy this file to a system with a browser and install the "
315 "cert into the browser."
316 )
317 print(
318 "You will then be able to test redfish and webui "
319 "authentication using this certificate."
320 )
321 print(
322 "Note: this p12 file was generated without a password, so it "
323 "can be imported easily."
324 )
325
326 caCertJSON = {
327 "CertificateString": caCertDump.decode(),
328 "CertificateType": "PEM",
329 }
330 caCertPath = "/redfish/v1/Managers/bmc/Truststore/Certificates"
331 replaceCertPath = "/redfish/v1/CertificateService/Actions/"
332 replaceCertPath += "CertificateService.ReplaceCertificate"
333 print("Attempting to install CA certificate to BMC.")
334
Alex Schendel47af8322023-07-21 13:32:38 -0700335 response = redfishObject.post(caCertPath, body=caCertJSON)
336 if response.status == 500:
337 print(
338 "An existing CA certificate is likely already installed."
339 " Replacing..."
340 )
Ed Tanous7b9e2562024-04-07 20:24:12 -0700341 caCertJSON["CertificateUri"] = {
342 "@odata.id": caCertPath + "/1",
343 }
344
Alex Schendel47af8322023-07-21 13:32:38 -0700345 response = redfishObject.post(replaceCertPath, body=caCertJSON)
346 if response.status == 200:
347 print("Successfully replaced existing CA certificate.")
348 else:
349 raise Exception(
350 "Could not install or replace CA certificate."
351 "Please check if a certificate is already installed. If a"
352 "certificate is already installed, try performing a factory"
353 "restore to clear such settings."
354 )
355 elif response.status == 200:
356 print("Successfully installed CA certificate.")
357 else:
358 raise Exception("Could not install certificate: " + response.read)
Ed Tanous7b9e2562024-04-07 20:24:12 -0700359 serverCertJSON = {
360 "CertificateString": serverKeyDumpStr + serverCertDumpStr,
361 "CertificateUri": {
362 "@odata.id": "/redfish/v1/Managers/bmc/NetworkProtocol/HTTPS/Certificates/1",
363 },
364 "CertificateType": "PEM",
365 }
366
Alex Schendel47af8322023-07-21 13:32:38 -0700367 print("Replacing server certificate...")
368 response = redfishObject.post(replaceCertPath, body=serverCertJSON)
369 if response.status == 200:
370 print("Successfully replaced server certificate.")
371 else:
372 raise Exception("Could not replace certificate: " + response.read)
373 tlsPatchJSON = {"Oem": {"OpenBMC": {"AuthMethods": {"TLS": True}}}}
374 print("Ensuring TLS authentication is enabled.")
375 response = redfishObject.patch(
376 "/redfish/v1/AccountService", body=tlsPatchJSON
377 )
378 if response.status == 200:
379 print("Successfully enabled TLS authentication.")
380 else:
381 raise Exception("Could not enable TLS auth: " + response.read)
382 redfishObject.logout()
383 print("Testing redfish TLS authentication with generated certs.")
384 response = requests.get(
Ed Tanous7b9e2562024-04-07 20:24:12 -0700385 f"https://{url.netloc}/redfish/v1/SessionService/Sessions",
386 verify=os.path.join(certsDir, "CA-cert.cer"),
387 cert=(
388 os.path.join(certsDir, "client-cert.pem"),
389 os.path.join(certsDir, "client-key.pem"),
390 ),
Alex Schendel47af8322023-07-21 13:32:38 -0700391 )
392 response.raise_for_status()
393 print("Redfish TLS authentication success!")
Alex Schendel47af8322023-07-21 13:32:38 -0700394
395
Ed Tanous7b9e2562024-04-07 20:24:12 -0700396if __name__ == "__main__":
397 main()