blob: 1ab828c380c2a965a6e8d69eb219ca4fb44bdd67 [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
Malik Akbar Hashemi Rafsanjani5889d792025-08-13 08:04:02 -0700385def test_mtls_auth(url, certs_dir, use_http2):
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 Rafsanjani5889d792025-08-13 08:04:02 -0700393 http2=use_http2,
Malik Akbar Hashemi Rafsanjaniaca5a542025-03-12 10:43:48 -0700394 ) as client:
395 print("Testing mTLS auth with CommonName")
396 response = client.get(
397 "/redfish/v1/SessionService/Sessions",
398 )
399 response.raise_for_status()
400
401 print("Changing CertificateMappingAttribute to use UPN")
402 patch_json = {
403 "MultiFactorAuth": {
404 "ClientCertificate": {
405 "CertificateMappingAttribute": "UserPrincipalName"
406 }
407 }
408 }
409 response = client.patch(
410 "/redfish/v1/AccountService",
411 json=patch_json,
412 )
413 response.raise_for_status()
414
415 with httpx.Client(
416 base_url=f"https://{url}",
417 verify=os.path.join(certs_dir, "CA-cert.cer"),
418 cert=(
419 os.path.join(certs_dir, "upn-client-cert.pem"),
420 os.path.join(certs_dir, "upn-client-key.pem"),
421 ),
Malik Akbar Hashemi Rafsanjani5889d792025-08-13 08:04:02 -0700422 http2=use_http2,
Malik Akbar Hashemi Rafsanjaniaca5a542025-03-12 10:43:48 -0700423 ) as client:
424 print("Retesting mTLS auth with UPN")
425 response = client.get(
426 "/redfish/v1/SessionService/Sessions",
427 )
428 response.raise_for_status()
429
430 print("Changing CertificateMappingAttribute to use CommonName")
431 patch_json = {
432 "MultiFactorAuth": {
433 "ClientCertificate": {
434 "CertificateMappingAttribute": "CommonName"
435 }
436 }
437 }
438 response = client.patch(
439 "/redfish/v1/AccountService",
440 json=patch_json,
441 )
442 response.raise_for_status()
Ed Tanous178c55a2025-02-11 15:43:33 -0800443
444
445def setup_server_cert(
446 redfish_session,
447 ca_cert_dump,
448 certs_dir,
449 client_key,
450 client_cert,
451 username,
452 url,
Ben Peled34b7ca42025-04-08 09:16:34 -0700453 server_intermediate_key,
454 server_intermediate_cert,
Ed Tanous178c55a2025-02-11 15:43:33 -0800455):
456 service_root = redfish_session.get("/redfish/v1/")
457 service_root.raise_for_status()
458
459 manager_uri = service_root.json()["Links"]["ManagerProvidingService"][
460 "@odata.id"
461 ]
462
463 install_ca_cert(redfish_session, ca_cert_dump, manager_uri)
464 generate_pk12(certs_dir, client_key, client_cert, username)
465
466 csr = generateCsr(
467 redfish_session,
468 url,
469 manager_uri,
470 )
471 serverCert = generateServerCert(
472 url,
Ben Peled34b7ca42025-04-08 09:16:34 -0700473 server_intermediate_key,
474 server_intermediate_cert,
Ed Tanous178c55a2025-02-11 15:43:33 -0800475 csr,
476 )
477 server_cert_dump = serverCert.public_bytes(
478 encoding=serialization.Encoding.PEM
479 )
Ben Peled34b7ca42025-04-08 09:16:34 -0700480
481 # If using intermediate certificate, save server cert without intermediate for debugging
482 if (
483 isinstance(server_intermediate_cert, x509.Certificate)
484 and server_intermediate_cert.subject != server_intermediate_cert.issuer
485 ):
486 with open(os.path.join(certs_dir, "server-cert-only.pem"), "wb") as f:
487 f.write(server_cert_dump)
488 print("Server cert (without intermediate) saved for debugging.")
489 intermediate_cert_dump = server_intermediate_cert.public_bytes(
490 encoding=serialization.Encoding.PEM
491 )
492 server_cert_dump = server_cert_dump + intermediate_cert_dump
493
Ed Tanous178c55a2025-02-11 15:43:33 -0800494 with open(os.path.join(certs_dir, "server-cert.pem"), "wb") as f:
495 f.write(server_cert_dump)
496 print("Server cert generated.")
497
498 install_server_cert(redfish_session, manager_uri, server_cert_dump)
499
Malik Akbar Hashemi Rafsanjaniaca5a542025-03-12 10:43:48 -0700500 print("Make sure setting CertificateMappingAttribute to CommonName")
501 patch_json = {
502 "MultiFactorAuth": {
503 "ClientCertificate": {"CertificateMappingAttribute": "CommonName"}
504 }
505 }
506 response = redfish_session.patch(
507 "/redfish/v1/AccountService", json=patch_json
508 )
509 response.raise_for_status()
510
Ed Tanous178c55a2025-02-11 15:43:33 -0800511
Ben Peled34b7ca42025-04-08 09:16:34 -0700512def generateIntermediateCA(ca_key, ca_cert, name_prefix):
513 private_key = ec.generate_private_key(ec.SECP256R1())
514 public_key = private_key.public_key()
515 builder = x509.CertificateBuilder()
516
517 name = x509.Name(
518 [
519 x509.NameAttribute(NameOID.COUNTRY_NAME, "US"),
520 x509.NameAttribute(NameOID.STATE_OR_PROVINCE_NAME, "California"),
521 x509.NameAttribute(NameOID.LOCALITY_NAME, "Santa Clara"),
522 x509.NameAttribute(NameOID.ORGANIZATION_NAME, "OpenBMC"),
523 x509.NameAttribute(NameOID.ORGANIZATIONAL_UNIT_NAME, "bmcweb"),
524 x509.NameAttribute(NameOID.COMMON_NAME, "Test Intermediate"),
525 ]
526 )
527 builder = builder.subject_name(name)
528 builder = builder.issuer_name(ca_cert.subject)
529
530 builder = builder.not_valid_before(
531 datetime.datetime(1970, 1, 1, 0, 0, tzinfo=datetime.timezone.utc)
532 )
533 builder = builder.not_valid_after(
534 datetime.datetime(2070, 1, 1, 0, 0, tzinfo=datetime.timezone.utc)
535 )
536
537 builder = builder.serial_number(x509.random_serial_number())
538 builder = builder.public_key(public_key)
539
540 basic_constraints = x509.BasicConstraints(ca=True, path_length=0)
541 builder = builder.add_extension(basic_constraints, critical=True)
542
543 usage = x509.KeyUsage(
544 content_commitment=False,
545 crl_sign=True,
546 data_encipherment=False,
547 decipher_only=False,
548 digital_signature=False,
549 encipher_only=False,
550 key_agreement=False,
551 key_cert_sign=True,
552 key_encipherment=False,
553 )
554 builder = builder.add_extension(usage, critical=True)
555
556 auth_key = x509.AuthorityKeyIdentifier.from_issuer_public_key(public_key)
557 builder = builder.add_extension(auth_key, critical=False)
558
559 intermediate_cert = builder.sign(
560 private_key=ca_key, algorithm=hashes.SHA256()
561 )
562
563 return private_key, intermediate_cert
564
565
Malik Akbar Hashemi Rafsanjani5889d792025-08-13 08:04:02 -0700566def generate_and_load_certs(
567 url, username, password, use_http2, use_intermediate=False
568):
Ed Tanous178c55a2025-02-11 15:43:33 -0800569 certs_dir = os.path.expanduser("~/certs")
570 print(f"Writing certs to {certs_dir}")
571 try:
572 print("Making certs directory.")
573 os.mkdir(certs_dir)
574 except OSError as error:
575 if error.errno != errno.EEXIST:
576 raise
577
578 ca_cert_filename = os.path.join(certs_dir, "CA-cert.cer")
579 ca_key_filename = os.path.join(certs_dir, "CA-key.pem")
580 if not os.path.exists(ca_cert_filename):
581 ca_key, ca_cert = generateCA()
582
583 ca_key_dump = ca_key.private_bytes(
584 encoding=serialization.Encoding.PEM,
585 format=serialization.PrivateFormat.TraditionalOpenSSL,
586 encryption_algorithm=serialization.NoEncryption(),
587 )
588 ca_cert_dump = ca_cert.public_bytes(
589 encoding=serialization.Encoding.PEM
590 )
591
592 with open(ca_cert_filename, "wb") as f:
593 f.write(ca_cert_dump)
594 print("CA cert generated.")
595 with open(ca_key_filename, "wb") as f:
596 f.write(ca_key_dump)
597 print("CA key generated.")
598
599 with open(ca_cert_filename, "rb") as ca_cert_file:
600 ca_cert_dump = ca_cert_file.read()
601 ca_cert = x509.load_pem_x509_certificate(ca_cert_dump)
602
603 with open(ca_key_filename, "rb") as ca_key_file:
604 ca_key_dump = ca_key_file.read()
605 ca_key = load_pem_private_key(ca_key_dump, None)
606
Ben Peled34b7ca42025-04-08 09:16:34 -0700607 # Generate intermediate certificates if requested
608 if use_intermediate:
609 # Generate client intermediate
610 client_intermediate_key, client_intermediate_cert = (
611 generateIntermediateCA(ca_key, ca_cert, "client")
612 )
613 # ... save client intermediate certs ...
614
615 # Generate server intermediate
616 server_intermediate_key, server_intermediate_cert = (
617 generateIntermediateCA(ca_key, ca_cert, "server")
618 )
619 else:
620 client_intermediate_key = ca_key
621 client_intermediate_cert = ca_cert
622 server_intermediate_key = ca_key
623 server_intermediate_cert = ca_cert
624
625 # Update client cert generation to use intermediate
Ed Tanous178c55a2025-02-11 15:43:33 -0800626 client_key, client_cert = generate_client_key_and_cert(
Ben Peled34b7ca42025-04-08 09:16:34 -0700627 ca_cert=client_intermediate_cert,
628 ca_key=client_intermediate_key,
629 common_name=username,
Ed Tanous178c55a2025-02-11 15:43:33 -0800630 )
631 client_key_dump = client_key.private_bytes(
632 encoding=serialization.Encoding.PEM,
633 format=serialization.PrivateFormat.TraditionalOpenSSL,
634 encryption_algorithm=serialization.NoEncryption(),
635 )
636
637 with open(os.path.join(certs_dir, "client-key.pem"), "wb") as f:
638 f.write(client_key_dump)
639 print("Client key generated.")
640 client_cert_dump = client_cert.public_bytes(
641 encoding=serialization.Encoding.PEM
642 )
643
Ben Peled34b7ca42025-04-08 09:16:34 -0700644 # Save client certificate without intermediate for debugging
645 if use_intermediate:
646 with open(os.path.join(certs_dir, "client-cert-only.pem"), "wb") as f:
647 f.write(client_cert_dump)
648 print("Client cert (without intermediate) saved for debugging.")
649 client_cert_dump = (
650 client_cert_dump
651 + client_intermediate_cert.public_bytes(
652 encoding=serialization.Encoding.PEM
653 )
654 ) # Append intermediate to create chain
655
Ed Tanous178c55a2025-02-11 15:43:33 -0800656 with open(os.path.join(certs_dir, "client-cert.pem"), "wb") as f:
657 f.write(client_cert_dump)
658 print("Client cert generated.")
659
660 print(f"Connecting to {url}")
661 with httpx.Client(
Malik Akbar Hashemi Rafsanjani5889d792025-08-13 08:04:02 -0700662 base_url=f"https://{url}",
663 verify=False,
664 follow_redirects=False,
665 http2=use_http2,
Ed Tanous178c55a2025-02-11 15:43:33 -0800666 ) as redfish_session:
667 with RedfishSessionContext(
668 redfish_session, username, password
669 ) as rf_session:
670 redfish_session.headers["X-Auth-Token"] = rf_session.x_auth_token
Malik Akbar Hashemi Rafsanjaniaca5a542025-03-12 10:43:48 -0700671
672 hostname = get_hostname(redfish_session, username, password, url)
673 print(f"Hostname: {hostname}")
674
675 upn_client_key, upn_client_cert = generate_client_key_and_cert(
676 ca_cert,
677 ca_key,
678 upn=f"{username}@{hostname}",
679 )
680 upn_client_key_dump = upn_client_key.private_bytes(
681 encoding=serialization.Encoding.PEM,
682 format=serialization.PrivateFormat.TraditionalOpenSSL,
683 encryption_algorithm=serialization.NoEncryption(),
684 )
685 with open(
686 os.path.join(certs_dir, "upn-client-key.pem"), "wb"
687 ) as f:
688 f.write(upn_client_key_dump)
689 print("UPN client key generated.")
690
691 upn_client_cert_dump = upn_client_cert.public_bytes(
692 encoding=serialization.Encoding.PEM
693 )
694 with open(
695 os.path.join(certs_dir, "upn-client-cert.pem"), "wb"
696 ) as f:
697 f.write(upn_client_cert_dump)
698 print("UPN client cert generated.")
699
Ed Tanous178c55a2025-02-11 15:43:33 -0800700 setup_server_cert(
701 redfish_session,
702 ca_cert_dump,
703 certs_dir,
704 client_key,
705 client_cert,
706 username,
707 url,
Ben Peled34b7ca42025-04-08 09:16:34 -0700708 server_intermediate_key,
709 server_intermediate_cert,
Ed Tanous178c55a2025-02-11 15:43:33 -0800710 )
711
712 print("Testing redfish TLS authentication with generated certs.")
Ed Tanous178c55a2025-02-11 15:43:33 -0800713 time.sleep(2)
Malik Akbar Hashemi Rafsanjani5889d792025-08-13 08:04:02 -0700714 test_mtls_auth(url, certs_dir, use_http2)
Alex Schendel47af8322023-07-21 13:32:38 -0700715 print("Redfish TLS authentication success!")
Alex Schendel47af8322023-07-21 13:32:38 -0700716
717
Ed Tanous178c55a2025-02-11 15:43:33 -0800718def main():
719 parser = argparse.ArgumentParser()
720 parser.add_argument(
721 "--username",
722 help="Username to connect with",
723 default="root",
724 )
725 parser.add_argument(
726 "--password",
727 help="Password for user in order to install certs over Redfish.",
728 default="0penBmc",
729 )
Ben Peled34b7ca42025-04-08 09:16:34 -0700730 parser.add_argument(
731 "--use-intermediate",
732 action="store_true",
733 default=False,
734 help="Generate and use an intermediate certificate",
735 )
Malik Akbar Hashemi Rafsanjani5889d792025-08-13 08:04:02 -0700736 parser.add_argument(
Malik Akbar Hashemi Rafsanjaniac2ff472025-08-13 11:55:57 -0700737 "--http2",
738 action=argparse.BooleanOptionalAction,
Malik Akbar Hashemi Rafsanjani5889d792025-08-13 08:04:02 -0700739 default=False,
Malik Akbar Hashemi Rafsanjaniac2ff472025-08-13 11:55:57 -0700740 help="Use HTTP2 for testing",
Malik Akbar Hashemi Rafsanjani5889d792025-08-13 08:04:02 -0700741 )
Ed Tanous178c55a2025-02-11 15:43:33 -0800742 parser.add_argument("host", help="Host to connect to")
743
744 args = parser.parse_args()
Ben Peled34b7ca42025-04-08 09:16:34 -0700745 generate_and_load_certs(
Malik Akbar Hashemi Rafsanjani5889d792025-08-13 08:04:02 -0700746 args.host,
747 args.username,
748 args.password,
Malik Akbar Hashemi Rafsanjaniac2ff472025-08-13 11:55:57 -0700749 args.http2,
Malik Akbar Hashemi Rafsanjani5889d792025-08-13 08:04:02 -0700750 args.use_intermediate,
Ben Peled34b7ca42025-04-08 09:16:34 -0700751 )
Ed Tanous178c55a2025-02-11 15:43:33 -0800752
753
Ed Tanous7b9e2562024-04-07 20:24:12 -0700754if __name__ == "__main__":
755 main()