blob: 1cba4644c39952c16d31fbf89a340317b53724ba [file] [log] [blame]
Alex Schendel47af8322023-07-21 13:32:38 -07001#!/usr/bin/env python3
Ed Tanous178c55a2025-02-11 15:43:33 -08002"""
3Script to generate certificates for a CA, server, and client allowing for
4client authentication using mTLS certificates. This can then be used to test
5mTLS client authentication for Redfish and WebUI.
6"""
Alex Schendel47af8322023-07-21 13:32:38 -07007
8import argparse
Ed Tanous178c55a2025-02-11 15:43:33 -08009import datetime
10import errno
11import ipaddress
Alex Schendel47af8322023-07-21 13:32:38 -070012import os
Ed Tanous7b9e2562024-04-07 20:24:12 -070013import socket
Ed Tanous178c55a2025-02-11 15:43:33 -080014import time
Malik Akbar Hashemi Rafsanjaniaca5a542025-03-12 10:43:48 -070015from typing import Optional
Alex Schendel47af8322023-07-21 13:32:38 -070016
Malik Akbar Hashemi Rafsanjaniaca5a542025-03-12 10:43:48 -070017import asn1
Ed Tanous178c55a2025-02-11 15:43:33 -080018import httpx
19from cryptography import x509
20from cryptography.hazmat.primitives import hashes, serialization
21from cryptography.hazmat.primitives.asymmetric import ec
22from cryptography.hazmat.primitives.serialization import (
23 load_pem_private_key,
24 pkcs12,
25)
26from cryptography.x509.oid import NameOID
Alex Schendel47af8322023-07-21 13:32:38 -070027
Ed Tanous178c55a2025-02-11 15:43:33 -080028replaceCertPath = "/redfish/v1/CertificateService/Actions/CertificateService.ReplaceCertificate"
Malik Akbar Hashemi Rafsanjaniaca5a542025-03-12 10:43:48 -070029# https://oidref.com/1.3.6.1.4.1.311.20.2.3
30upnObjectIdentifier = "1.3.6.1.4.1.311.20.2.3"
Alex Schendel47af8322023-07-21 13:32:38 -070031
32
Ed Tanous178c55a2025-02-11 15:43:33 -080033class RedfishSessionContext:
34 def __init__(self, client, username="root", password="0penBmc"):
35 self.client = client
36 self.session_uri = None
37 self.x_auth_token = None
38 self.username = username
39 self.password = password
Alex Schendel47af8322023-07-21 13:32:38 -070040
Ed Tanous178c55a2025-02-11 15:43:33 -080041 def __enter__(self):
42 r = self.client.post(
43 "/redfish/v1/SessionService/Sessions",
44 json={
45 "UserName": self.username,
46 "Password": self.password,
47 "Context": f"pythonscript::{os.path.basename(__file__)}",
48 },
49 headers={"content-type": "application/json"},
50 )
51 r.raise_for_status()
52 self.x_auth_token = r.headers["x-auth-token"]
53 self.session_uri = r.headers["location"]
54 return self
Ed Tanous7b9e2562024-04-07 20:24:12 -070055
Ed Tanous178c55a2025-02-11 15:43:33 -080056 def __exit__(self, type, value, traceback):
57 if not self.session_uri:
58 return
59 r = self.client.delete(self.session_uri)
60 r.raise_for_status()
Alex Schendel47af8322023-07-21 13:32:38 -070061
Alex Schendel47af8322023-07-21 13:32:38 -070062
Malik Akbar Hashemi Rafsanjaniaca5a542025-03-12 10:43:48 -070063def get_hostname(redfish_session, username, password, url):
64 service_root = redfish_session.get("/redfish/v1/")
65 service_root.raise_for_status()
66
67 manager_uri = service_root.json()["Links"]["ManagerProvidingService"][
68 "@odata.id"
69 ]
70
71 manager_response = redfish_session.get(manager_uri)
72 manager_response.raise_for_status()
73
74 network_protocol_uri = manager_response.json()["NetworkProtocol"][
75 "@odata.id"
76 ]
77
78 network_protocol_response = redfish_session.get(network_protocol_uri)
79 network_protocol_response.raise_for_status()
80
81 hostname = network_protocol_response.json()["HostName"]
82
83 return hostname
84
85
Ed Tanous178c55a2025-02-11 15:43:33 -080086def generateCA():
87 private_key = ec.generate_private_key(ec.SECP256R1())
88 public_key = private_key.public_key()
89 builder = x509.CertificateBuilder()
90
91 name = x509.Name(
Alex Schendel47af8322023-07-21 13:32:38 -070092 [
Ed Tanous178c55a2025-02-11 15:43:33 -080093 x509.NameAttribute(NameOID.ORGANIZATION_NAME, "OpenBMC"),
94 x509.NameAttribute(NameOID.ORGANIZATIONAL_UNIT_NAME, "bmcweb"),
95 x509.NameAttribute(NameOID.COMMON_NAME, "Test CA"),
Alex Schendel47af8322023-07-21 13:32:38 -070096 ]
97 )
Ed Tanous178c55a2025-02-11 15:43:33 -080098 builder = builder.subject_name(name)
99 builder = builder.issuer_name(name)
100
101 builder = builder.not_valid_before(
102 datetime.datetime(1970, 1, 1, 0, 0, tzinfo=datetime.timezone.utc)
103 )
104 builder = builder.not_valid_after(
105 datetime.datetime(2070, 1, 1, 0, 0, tzinfo=datetime.timezone.utc)
106 )
107 builder = builder.serial_number(x509.random_serial_number())
108 builder = builder.public_key(public_key)
109
110 basic_constraints = x509.BasicConstraints(ca=True, path_length=None)
111 builder = builder.add_extension(basic_constraints, critical=True)
112
113 usage = x509.KeyUsage(
114 content_commitment=False,
115 crl_sign=True,
116 data_encipherment=False,
117 decipher_only=False,
118 digital_signature=False,
119 encipher_only=False,
120 key_agreement=False,
121 key_cert_sign=True,
122 key_encipherment=False,
123 )
124 builder = builder.add_extension(usage, critical=False)
125
126 auth_key = x509.AuthorityKeyIdentifier.from_issuer_public_key(public_key)
127
128 builder = builder.add_extension(auth_key, critical=False)
129
130 root_cert = builder.sign(
131 private_key=private_key, algorithm=hashes.SHA256()
Alex Schendel47af8322023-07-21 13:32:38 -0700132 )
133
Ed Tanous178c55a2025-02-11 15:43:33 -0800134 return private_key, root_cert
Alex Schendel47af8322023-07-21 13:32:38 -0700135
136
Ed Tanous178c55a2025-02-11 15:43:33 -0800137def signCsr(csr, ca_key):
138 csr.sign(ca_key, algorithm=hashes.SHA256())
139 return
140
141
Malik Akbar Hashemi Rafsanjaniaca5a542025-03-12 10:43:48 -0700142def generate_client_key_and_cert(
143 ca_cert,
144 ca_key,
145 common_name: Optional[str] = None,
146 upn: Optional[str] = None,
147):
Ed Tanous178c55a2025-02-11 15:43:33 -0800148 private_key = ec.generate_private_key(ec.SECP256R1())
149 public_key = private_key.public_key()
150 builder = x509.CertificateBuilder()
151
Malik Akbar Hashemi Rafsanjaniaca5a542025-03-12 10:43:48 -0700152 cert_names = [
153 x509.NameAttribute(NameOID.COUNTRY_NAME, "US"),
154 x509.NameAttribute(NameOID.STATE_OR_PROVINCE_NAME, "California"),
155 x509.NameAttribute(NameOID.LOCALITY_NAME, "San Francisco"),
156 x509.NameAttribute(NameOID.ORGANIZATION_NAME, "OpenBMC"),
157 x509.NameAttribute(NameOID.ORGANIZATIONAL_UNIT_NAME, "bmcweb"),
158 ]
159 if common_name is not None:
160 cert_names.append(x509.NameAttribute(NameOID.COMMON_NAME, common_name))
161
162 builder = builder.subject_name(x509.Name(cert_names))
Ed Tanous178c55a2025-02-11 15:43:33 -0800163
164 builder = builder.issuer_name(ca_cert.subject)
165 builder = builder.public_key(public_key)
166 builder = builder.serial_number(x509.random_serial_number())
167 builder = builder.not_valid_before(
168 datetime.datetime(1970, 1, 1, 0, 0, tzinfo=datetime.timezone.utc)
169 )
170 builder = builder.not_valid_after(
171 datetime.datetime(2070, 1, 1, 0, 0, tzinfo=datetime.timezone.utc)
172 )
173
174 usage = x509.KeyUsage(
175 content_commitment=False,
176 crl_sign=False,
177 data_encipherment=False,
178 decipher_only=False,
179 digital_signature=True,
180 encipher_only=False,
181 key_agreement=True,
182 key_cert_sign=False,
183 key_encipherment=False,
184 )
185 builder = builder.add_extension(usage, critical=False)
186
187 exusage = x509.ExtendedKeyUsage([x509.oid.ExtendedKeyUsageOID.CLIENT_AUTH])
188 builder = builder.add_extension(exusage, critical=True)
189
190 auth_key = x509.AuthorityKeyIdentifier.from_issuer_public_key(public_key)
191 builder = builder.add_extension(auth_key, critical=False)
192
Malik Akbar Hashemi Rafsanjaniaca5a542025-03-12 10:43:48 -0700193 if upn is not None:
194 encoder = asn1.Encoder()
195 encoder.start()
196 encoder.write(upn, asn1.Numbers.UTF8String)
197
198 builder = builder.add_extension(
199 x509.SubjectAlternativeName(
200 [
201 x509.OtherName(
202 x509.ObjectIdentifier(upnObjectIdentifier),
203 encoder.output(),
204 )
205 ]
206 ),
207 critical=False,
208 )
209
Ed Tanous178c55a2025-02-11 15:43:33 -0800210 signed = builder.sign(private_key=ca_key, algorithm=hashes.SHA256())
211
212 return private_key, signed
213
214
215def generateServerCert(url, ca_key, ca_cert, csr):
216 builder = x509.CertificateBuilder()
217
218 builder = builder.subject_name(csr.subject)
219 builder = builder.issuer_name(ca_cert.subject)
220 builder = builder.public_key(csr.public_key())
221 builder = builder.serial_number(x509.random_serial_number())
222 builder = builder.not_valid_before(
223 datetime.datetime(1970, 1, 1, 0, 0, tzinfo=datetime.timezone.utc)
224 )
225 builder = builder.not_valid_after(
226 datetime.datetime(2070, 1, 1, 0, 0, tzinfo=datetime.timezone.utc)
227 )
228
229 usage = x509.KeyUsage(
230 content_commitment=False,
231 crl_sign=False,
232 data_encipherment=False,
233 decipher_only=False,
234 digital_signature=True,
235 encipher_only=False,
236 key_agreement=False,
237 key_cert_sign=True,
238 key_encipherment=True,
239 )
240 builder = builder.add_extension(usage, critical=True)
241
242 exusage = x509.ExtendedKeyUsage([x509.oid.ExtendedKeyUsageOID.SERVER_AUTH])
243 builder = builder.add_extension(exusage, critical=True)
244
245 san_list = [x509.DNSName("localhost")]
246 try:
247 value = ipaddress.ip_address(url)
248 san_list.append(x509.IPAddress(value))
249 except ValueError:
250 san_list.append(x509.DNSName(url))
251
252 altname = x509.SubjectAlternativeName(san_list)
253 builder = builder.add_extension(altname, critical=True)
254 basic_constraints = x509.BasicConstraints(ca=False, path_length=None)
255 builder = builder.add_extension(basic_constraints, critical=True)
256
257 builder = builder.add_extension(
258 x509.SubjectKeyIdentifier.from_public_key(ca_key.public_key()),
259 critical=False,
260 )
261 authkeyident = x509.AuthorityKeyIdentifier.from_issuer_public_key(
262 ca_key.public_key()
263 )
264 builder = builder.add_extension(authkeyident, critical=False)
265
266 signed = builder.sign(private_key=ca_key, algorithm=hashes.SHA256())
267
268 return signed
269
270
271def generateCsr(
272 redfish_session,
273 commonName,
274 manager_uri,
Ed Tanous7b9e2562024-04-07 20:24:12 -0700275):
Ed Tanous7b9e2562024-04-07 20:24:12 -0700276 try:
277 socket.inet_aton(commonName)
278 commonName = "IP: " + commonName
279 except socket.error:
280 commonName = "DNS: " + commonName
281
282 CSRRequest = {
283 "CommonName": commonName,
284 "City": "San Fransisco",
285 "Country": "US",
286 "Organization": "",
287 "OrganizationalUnit": "",
288 "State": "CA",
289 "CertificateCollection": {
Ed Tanous178c55a2025-02-11 15:43:33 -0800290 "@odata.id": f"{manager_uri}/NetworkProtocol/HTTPS/Certificates",
Ed Tanous7b9e2562024-04-07 20:24:12 -0700291 },
292 "AlternativeNames": [
293 commonName,
294 "DNS: localhost",
295 "IP: 127.0.0.1",
296 ],
297 }
298
Ed Tanous178c55a2025-02-11 15:43:33 -0800299 response = redfish_session.post(
Ed Tanous7b9e2562024-04-07 20:24:12 -0700300 "/redfish/v1/CertificateService/Actions/CertificateService.GenerateCSR",
Ed Tanous178c55a2025-02-11 15:43:33 -0800301 json=CSRRequest,
Ed Tanous7b9e2562024-04-07 20:24:12 -0700302 )
Ed Tanous178c55a2025-02-11 15:43:33 -0800303 response.raise_for_status()
Ed Tanous7b9e2562024-04-07 20:24:12 -0700304
Ed Tanous178c55a2025-02-11 15:43:33 -0800305 csrString = response.json()["CSRString"]
306 csr = x509.load_pem_x509_csr(csrString.encode())
307 if not csr.is_signature_valid:
308 raise Exception("CSR was not valid")
309 return csr
Ed Tanous7b9e2562024-04-07 20:24:12 -0700310
311
Ed Tanous178c55a2025-02-11 15:43:33 -0800312def install_ca_cert(redfish_session, ca_cert_dump, manager_uri):
313 ca_certJSON = {
314 "CertificateString": ca_cert_dump.decode(),
Ed Tanous7b9e2562024-04-07 20:24:12 -0700315 "CertificateType": "PEM",
316 }
Ed Tanous178c55a2025-02-11 15:43:33 -0800317 ca_certPath = f"{manager_uri}/Truststore/Certificates"
Ed Tanous7b9e2562024-04-07 20:24:12 -0700318 print("Attempting to install CA certificate to BMC.")
319
Ed Tanous178c55a2025-02-11 15:43:33 -0800320 response = redfish_session.post(ca_certPath, json=ca_certJSON)
321 if response.status_code == 500:
Alex Schendel47af8322023-07-21 13:32:38 -0700322 print(
323 "An existing CA certificate is likely already installed."
324 " Replacing..."
325 )
Ed Tanous178c55a2025-02-11 15:43:33 -0800326 ca_certJSON["CertificateUri"] = {
327 "@odata.id": ca_certPath + "/1",
Ed Tanous7b9e2562024-04-07 20:24:12 -0700328 }
329
Ed Tanous178c55a2025-02-11 15:43:33 -0800330 response = redfish_session.post(replaceCertPath, json=ca_certJSON)
331 if response.status_code == 200:
Alex Schendel47af8322023-07-21 13:32:38 -0700332 print("Successfully replaced existing CA certificate.")
333 else:
334 raise Exception(
335 "Could not install or replace CA certificate."
336 "Please check if a certificate is already installed. If a"
337 "certificate is already installed, try performing a factory"
338 "restore to clear such settings."
339 )
Ed Tanous178c55a2025-02-11 15:43:33 -0800340 response.raise_for_status()
341 print("Successfully installed CA certificate.")
342
343
344def install_server_cert(redfish_session, manager_uri, server_cert_dump):
345
346 server_cert_json = {
347 "CertificateString": server_cert_dump.decode(),
Ed Tanous7b9e2562024-04-07 20:24:12 -0700348 "CertificateUri": {
Ed Tanous178c55a2025-02-11 15:43:33 -0800349 "@odata.id": f"{manager_uri}/NetworkProtocol/HTTPS/Certificates/1",
Ed Tanous7b9e2562024-04-07 20:24:12 -0700350 },
351 "CertificateType": "PEM",
352 }
353
Alex Schendel47af8322023-07-21 13:32:38 -0700354 print("Replacing server certificate...")
Ed Tanous178c55a2025-02-11 15:43:33 -0800355 response = redfish_session.post(replaceCertPath, json=server_cert_json)
356 if response.status_code == 200:
Alex Schendel47af8322023-07-21 13:32:38 -0700357 print("Successfully replaced server certificate.")
358 else:
Ed Tanous178c55a2025-02-11 15:43:33 -0800359 raise Exception(f"Could not replace certificate: {response.json()}")
360
361 tls_patch_json = {"Oem": {"OpenBMC": {"AuthMethods": {"TLS": True}}}}
Alex Schendel47af8322023-07-21 13:32:38 -0700362 print("Ensuring TLS authentication is enabled.")
Ed Tanous178c55a2025-02-11 15:43:33 -0800363 response = redfish_session.patch(
364 "/redfish/v1/AccountService", json=tls_patch_json
Alex Schendel47af8322023-07-21 13:32:38 -0700365 )
Ed Tanous178c55a2025-02-11 15:43:33 -0800366 if response.status_code == 200:
Alex Schendel47af8322023-07-21 13:32:38 -0700367 print("Successfully enabled TLS authentication.")
368 else:
369 raise Exception("Could not enable TLS auth: " + response.read)
Ed Tanous178c55a2025-02-11 15:43:33 -0800370
371
372def generate_pk12(certs_dir, key, client_cert, username):
373 print("Generating p12 cert file for browser authentication.")
374 p12 = pkcs12.serialize_key_and_certificates(
375 username.encode(),
376 key,
377 client_cert,
378 None,
379 serialization.NoEncryption(),
380 )
381 with open(os.path.join(certs_dir, "client.p12"), "wb") as f:
382 f.write(p12)
383
384
385def test_mtls_auth(url, certs_dir):
Malik Akbar Hashemi Rafsanjaniaca5a542025-03-12 10:43:48 -0700386 with httpx.Client(
387 base_url=f"https://{url}",
Ed Tanous178c55a2025-02-11 15:43:33 -0800388 verify=os.path.join(certs_dir, "CA-cert.cer"),
Ed Tanous7b9e2562024-04-07 20:24:12 -0700389 cert=(
Ed Tanous178c55a2025-02-11 15:43:33 -0800390 os.path.join(certs_dir, "client-cert.pem"),
391 os.path.join(certs_dir, "client-key.pem"),
Ed Tanous7b9e2562024-04-07 20:24:12 -0700392 ),
Malik Akbar Hashemi Rafsanjaniaca5a542025-03-12 10:43:48 -0700393 ) as client:
394 print("Testing mTLS auth with CommonName")
395 response = client.get(
396 "/redfish/v1/SessionService/Sessions",
397 )
398 response.raise_for_status()
399
400 print("Changing CertificateMappingAttribute to use UPN")
401 patch_json = {
402 "MultiFactorAuth": {
403 "ClientCertificate": {
404 "CertificateMappingAttribute": "UserPrincipalName"
405 }
406 }
407 }
408 response = client.patch(
409 "/redfish/v1/AccountService",
410 json=patch_json,
411 )
412 response.raise_for_status()
413
414 with httpx.Client(
415 base_url=f"https://{url}",
416 verify=os.path.join(certs_dir, "CA-cert.cer"),
417 cert=(
418 os.path.join(certs_dir, "upn-client-cert.pem"),
419 os.path.join(certs_dir, "upn-client-key.pem"),
420 ),
421 ) as client:
422 print("Retesting mTLS auth with UPN")
423 response = client.get(
424 "/redfish/v1/SessionService/Sessions",
425 )
426 response.raise_for_status()
427
428 print("Changing CertificateMappingAttribute to use CommonName")
429 patch_json = {
430 "MultiFactorAuth": {
431 "ClientCertificate": {
432 "CertificateMappingAttribute": "CommonName"
433 }
434 }
435 }
436 response = client.patch(
437 "/redfish/v1/AccountService",
438 json=patch_json,
439 )
440 response.raise_for_status()
Ed Tanous178c55a2025-02-11 15:43:33 -0800441
442
443def setup_server_cert(
444 redfish_session,
445 ca_cert_dump,
446 certs_dir,
447 client_key,
448 client_cert,
449 username,
450 url,
Ben Peled34b7ca42025-04-08 09:16:34 -0700451 server_intermediate_key,
452 server_intermediate_cert,
Ed Tanous178c55a2025-02-11 15:43:33 -0800453):
454 service_root = redfish_session.get("/redfish/v1/")
455 service_root.raise_for_status()
456
457 manager_uri = service_root.json()["Links"]["ManagerProvidingService"][
458 "@odata.id"
459 ]
460
461 install_ca_cert(redfish_session, ca_cert_dump, manager_uri)
462 generate_pk12(certs_dir, client_key, client_cert, username)
463
464 csr = generateCsr(
465 redfish_session,
466 url,
467 manager_uri,
468 )
469 serverCert = generateServerCert(
470 url,
Ben Peled34b7ca42025-04-08 09:16:34 -0700471 server_intermediate_key,
472 server_intermediate_cert,
Ed Tanous178c55a2025-02-11 15:43:33 -0800473 csr,
474 )
475 server_cert_dump = serverCert.public_bytes(
476 encoding=serialization.Encoding.PEM
477 )
Ben Peled34b7ca42025-04-08 09:16:34 -0700478
479 # If using intermediate certificate, save server cert without intermediate for debugging
480 if (
481 isinstance(server_intermediate_cert, x509.Certificate)
482 and server_intermediate_cert.subject != server_intermediate_cert.issuer
483 ):
484 with open(os.path.join(certs_dir, "server-cert-only.pem"), "wb") as f:
485 f.write(server_cert_dump)
486 print("Server cert (without intermediate) saved for debugging.")
487 intermediate_cert_dump = server_intermediate_cert.public_bytes(
488 encoding=serialization.Encoding.PEM
489 )
490 server_cert_dump = server_cert_dump + intermediate_cert_dump
491
Ed Tanous178c55a2025-02-11 15:43:33 -0800492 with open(os.path.join(certs_dir, "server-cert.pem"), "wb") as f:
493 f.write(server_cert_dump)
494 print("Server cert generated.")
495
496 install_server_cert(redfish_session, manager_uri, server_cert_dump)
497
Malik Akbar Hashemi Rafsanjaniaca5a542025-03-12 10:43:48 -0700498 print("Make sure setting CertificateMappingAttribute to CommonName")
499 patch_json = {
500 "MultiFactorAuth": {
501 "ClientCertificate": {"CertificateMappingAttribute": "CommonName"}
502 }
503 }
504 response = redfish_session.patch(
505 "/redfish/v1/AccountService", json=patch_json
506 )
507 response.raise_for_status()
508
Ed Tanous178c55a2025-02-11 15:43:33 -0800509
Ben Peled34b7ca42025-04-08 09:16:34 -0700510def generateIntermediateCA(ca_key, ca_cert, name_prefix):
511 private_key = ec.generate_private_key(ec.SECP256R1())
512 public_key = private_key.public_key()
513 builder = x509.CertificateBuilder()
514
515 name = x509.Name(
516 [
517 x509.NameAttribute(NameOID.COUNTRY_NAME, "US"),
518 x509.NameAttribute(NameOID.STATE_OR_PROVINCE_NAME, "California"),
519 x509.NameAttribute(NameOID.LOCALITY_NAME, "Santa Clara"),
520 x509.NameAttribute(NameOID.ORGANIZATION_NAME, "OpenBMC"),
521 x509.NameAttribute(NameOID.ORGANIZATIONAL_UNIT_NAME, "bmcweb"),
522 x509.NameAttribute(NameOID.COMMON_NAME, "Test Intermediate"),
523 ]
524 )
525 builder = builder.subject_name(name)
526 builder = builder.issuer_name(ca_cert.subject)
527
528 builder = builder.not_valid_before(
529 datetime.datetime(1970, 1, 1, 0, 0, tzinfo=datetime.timezone.utc)
530 )
531 builder = builder.not_valid_after(
532 datetime.datetime(2070, 1, 1, 0, 0, tzinfo=datetime.timezone.utc)
533 )
534
535 builder = builder.serial_number(x509.random_serial_number())
536 builder = builder.public_key(public_key)
537
538 basic_constraints = x509.BasicConstraints(ca=True, path_length=0)
539 builder = builder.add_extension(basic_constraints, critical=True)
540
541 usage = x509.KeyUsage(
542 content_commitment=False,
543 crl_sign=True,
544 data_encipherment=False,
545 decipher_only=False,
546 digital_signature=False,
547 encipher_only=False,
548 key_agreement=False,
549 key_cert_sign=True,
550 key_encipherment=False,
551 )
552 builder = builder.add_extension(usage, critical=True)
553
554 auth_key = x509.AuthorityKeyIdentifier.from_issuer_public_key(public_key)
555 builder = builder.add_extension(auth_key, critical=False)
556
557 intermediate_cert = builder.sign(
558 private_key=ca_key, algorithm=hashes.SHA256()
559 )
560
561 return private_key, intermediate_cert
562
563
564def generate_and_load_certs(url, username, password, use_intermediate=False):
Ed Tanous178c55a2025-02-11 15:43:33 -0800565 certs_dir = os.path.expanduser("~/certs")
566 print(f"Writing certs to {certs_dir}")
567 try:
568 print("Making certs directory.")
569 os.mkdir(certs_dir)
570 except OSError as error:
571 if error.errno != errno.EEXIST:
572 raise
573
574 ca_cert_filename = os.path.join(certs_dir, "CA-cert.cer")
575 ca_key_filename = os.path.join(certs_dir, "CA-key.pem")
576 if not os.path.exists(ca_cert_filename):
577 ca_key, ca_cert = generateCA()
578
579 ca_key_dump = ca_key.private_bytes(
580 encoding=serialization.Encoding.PEM,
581 format=serialization.PrivateFormat.TraditionalOpenSSL,
582 encryption_algorithm=serialization.NoEncryption(),
583 )
584 ca_cert_dump = ca_cert.public_bytes(
585 encoding=serialization.Encoding.PEM
586 )
587
588 with open(ca_cert_filename, "wb") as f:
589 f.write(ca_cert_dump)
590 print("CA cert generated.")
591 with open(ca_key_filename, "wb") as f:
592 f.write(ca_key_dump)
593 print("CA key generated.")
594
595 with open(ca_cert_filename, "rb") as ca_cert_file:
596 ca_cert_dump = ca_cert_file.read()
597 ca_cert = x509.load_pem_x509_certificate(ca_cert_dump)
598
599 with open(ca_key_filename, "rb") as ca_key_file:
600 ca_key_dump = ca_key_file.read()
601 ca_key = load_pem_private_key(ca_key_dump, None)
602
Ben Peled34b7ca42025-04-08 09:16:34 -0700603 # Generate intermediate certificates if requested
604 if use_intermediate:
605 # Generate client intermediate
606 client_intermediate_key, client_intermediate_cert = (
607 generateIntermediateCA(ca_key, ca_cert, "client")
608 )
609 # ... save client intermediate certs ...
610
611 # Generate server intermediate
612 server_intermediate_key, server_intermediate_cert = (
613 generateIntermediateCA(ca_key, ca_cert, "server")
614 )
615 else:
616 client_intermediate_key = ca_key
617 client_intermediate_cert = ca_cert
618 server_intermediate_key = ca_key
619 server_intermediate_cert = ca_cert
620
621 # Update client cert generation to use intermediate
Ed Tanous178c55a2025-02-11 15:43:33 -0800622 client_key, client_cert = generate_client_key_and_cert(
Ben Peled34b7ca42025-04-08 09:16:34 -0700623 ca_cert=client_intermediate_cert,
624 ca_key=client_intermediate_key,
625 common_name=username,
Ed Tanous178c55a2025-02-11 15:43:33 -0800626 )
627 client_key_dump = client_key.private_bytes(
628 encoding=serialization.Encoding.PEM,
629 format=serialization.PrivateFormat.TraditionalOpenSSL,
630 encryption_algorithm=serialization.NoEncryption(),
631 )
632
633 with open(os.path.join(certs_dir, "client-key.pem"), "wb") as f:
634 f.write(client_key_dump)
635 print("Client key generated.")
636 client_cert_dump = client_cert.public_bytes(
637 encoding=serialization.Encoding.PEM
638 )
639
Ben Peled34b7ca42025-04-08 09:16:34 -0700640 # Save client certificate without intermediate for debugging
641 if use_intermediate:
642 with open(os.path.join(certs_dir, "client-cert-only.pem"), "wb") as f:
643 f.write(client_cert_dump)
644 print("Client cert (without intermediate) saved for debugging.")
645 client_cert_dump = (
646 client_cert_dump
647 + client_intermediate_cert.public_bytes(
648 encoding=serialization.Encoding.PEM
649 )
650 ) # Append intermediate to create chain
651
Ed Tanous178c55a2025-02-11 15:43:33 -0800652 with open(os.path.join(certs_dir, "client-cert.pem"), "wb") as f:
653 f.write(client_cert_dump)
654 print("Client cert generated.")
655
656 print(f"Connecting to {url}")
657 with httpx.Client(
658 base_url=f"https://{url}", verify=False, follow_redirects=False
659 ) as redfish_session:
660 with RedfishSessionContext(
661 redfish_session, username, password
662 ) as rf_session:
663 redfish_session.headers["X-Auth-Token"] = rf_session.x_auth_token
Malik Akbar Hashemi Rafsanjaniaca5a542025-03-12 10:43:48 -0700664
665 hostname = get_hostname(redfish_session, username, password, url)
666 print(f"Hostname: {hostname}")
667
668 upn_client_key, upn_client_cert = generate_client_key_and_cert(
669 ca_cert,
670 ca_key,
671 upn=f"{username}@{hostname}",
672 )
673 upn_client_key_dump = upn_client_key.private_bytes(
674 encoding=serialization.Encoding.PEM,
675 format=serialization.PrivateFormat.TraditionalOpenSSL,
676 encryption_algorithm=serialization.NoEncryption(),
677 )
678 with open(
679 os.path.join(certs_dir, "upn-client-key.pem"), "wb"
680 ) as f:
681 f.write(upn_client_key_dump)
682 print("UPN client key generated.")
683
684 upn_client_cert_dump = upn_client_cert.public_bytes(
685 encoding=serialization.Encoding.PEM
686 )
687 with open(
688 os.path.join(certs_dir, "upn-client-cert.pem"), "wb"
689 ) as f:
690 f.write(upn_client_cert_dump)
691 print("UPN client cert generated.")
692
Ed Tanous178c55a2025-02-11 15:43:33 -0800693 setup_server_cert(
694 redfish_session,
695 ca_cert_dump,
696 certs_dir,
697 client_key,
698 client_cert,
699 username,
700 url,
Ben Peled34b7ca42025-04-08 09:16:34 -0700701 server_intermediate_key,
702 server_intermediate_cert,
Ed Tanous178c55a2025-02-11 15:43:33 -0800703 )
704
705 print("Testing redfish TLS authentication with generated certs.")
Ed Tanous178c55a2025-02-11 15:43:33 -0800706 time.sleep(2)
707 test_mtls_auth(url, certs_dir)
Alex Schendel47af8322023-07-21 13:32:38 -0700708 print("Redfish TLS authentication success!")
Alex Schendel47af8322023-07-21 13:32:38 -0700709
710
Ed Tanous178c55a2025-02-11 15:43:33 -0800711def main():
712 parser = argparse.ArgumentParser()
713 parser.add_argument(
714 "--username",
715 help="Username to connect with",
716 default="root",
717 )
718 parser.add_argument(
719 "--password",
720 help="Password for user in order to install certs over Redfish.",
721 default="0penBmc",
722 )
Ben Peled34b7ca42025-04-08 09:16:34 -0700723 parser.add_argument(
724 "--use-intermediate",
725 action="store_true",
726 default=False,
727 help="Generate and use an intermediate certificate",
728 )
Ed Tanous178c55a2025-02-11 15:43:33 -0800729 parser.add_argument("host", help="Host to connect to")
730
731 args = parser.parse_args()
Ben Peled34b7ca42025-04-08 09:16:34 -0700732 generate_and_load_certs(
733 args.host, args.username, args.password, args.use_intermediate
734 )
Ed Tanous178c55a2025-02-11 15:43:33 -0800735
736
Ed Tanous7b9e2562024-04-07 20:24:12 -0700737if __name__ == "__main__":
738 main()