1#!/usr/bin/env python3 2 3import argparse 4import os 5import socket 6import urllib 7 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 19SCRIPT_DIR = os.path.dirname(os.path.realpath(__file__)) 20 21# 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) 38 39 cert.set_notBefore(b"19700101000000Z") 40 cert.set_notAfter(b"20700101000000Z") 41 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 66 ), 67 ] 68 ) 69 70 # sign CA cert with CA key 71 cert.sign(key, "sha256") 72 return key, cert 73 74 75def 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): 116 # key 117 118 # cert 119 cert = crypto.X509() 120 cert.set_serial_number(serial) 121 cert.set_version(2) 122 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") 134 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 144 extensions.extend( 145 [ 146 crypto.X509Extension( 147 b"authorityKeyIdentifier", False, b"keyid", issuer=caCert 148 ), 149 ] 150 ) 151 cert.add_extensions(extensions) 152 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 ) 177 if "//" not in host: 178 host = f"https://{host}" 179 url = urllib.parse.urlparse(host, scheme="https") 180 181 serial = 1000 182 certsDir = os.path.join(SCRIPT_DIR, "certs") 183 print(f"Writing certs to {certsDir}") 184 try: 185 print("Making certs directory.") 186 os.mkdir(certsDir) 187 except OSError as error: 188 if error.errno == 17: 189 print("certs directory already exists. Skipping...") 190 else: 191 print(error) 192 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.") 213 serial += 1 214 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 ] 224 225 redfishObject = redfish.redfish_client( 226 base_url="https://" + url.netloc, 227 username=username, 228 password=password, 229 default_prefix="/redfish/v1", 230 ) 231 redfishObject.login(auth="session") 232 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 335 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 ) 341 caCertJSON["CertificateUri"] = { 342 "@odata.id": caCertPath + "/1", 343 } 344 345 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) 359 serverCertJSON = { 360 "CertificateString": serverKeyDumpStr + serverCertDumpStr, 361 "CertificateUri": { 362 "@odata.id": "/redfish/v1/Managers/bmc/NetworkProtocol/HTTPS/Certificates/1", 363 }, 364 "CertificateType": "PEM", 365 } 366 367 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( 385 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 ), 391 ) 392 response.raise_for_status() 393 print("Redfish TLS authentication success!") 394 395 396if __name__ == "__main__": 397 main() 398