Alex Schendel | 47af832 | 2023-07-21 13:32:38 -0700 | [diff] [blame] | 1 | #!/usr/bin/env python3 |
| 2 | |
| 3 | import argparse |
| 4 | import os |
Ed Tanous | 7b9e256 | 2024-04-07 20:24:12 -0700 | [diff] [blame] | 5 | import socket |
| 6 | import urllib |
Alex Schendel | 47af832 | 2023-07-21 13:32:38 -0700 | [diff] [blame] | 7 | |
| 8 | import requests |
| 9 | |
| 10 | try: |
| 11 | import redfish |
| 12 | except ModuleNotFoundError: |
| 13 | raise Exception("Please run pip install redfish to run this script.") |
| 14 | try: |
| 15 | from OpenSSL import crypto |
| 16 | except ImportError: |
| 17 | raise Exception("Please run pip install pyOpenSSL to run this script.") |
| 18 | |
Ed Tanous | 7b9e256 | 2024-04-07 20:24:12 -0700 | [diff] [blame] | 19 | SCRIPT_DIR = os.path.dirname(os.path.realpath(__file__)) |
| 20 | |
Alex Schendel | 47af832 | 2023-07-21 13:32:38 -0700 | [diff] [blame] | 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 | |
| 28 | def 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 Tanous | 7b9e256 | 2024-04-07 20:24:12 -0700 | [diff] [blame] | 38 | |
| 39 | cert.set_notBefore(b"19700101000000Z") |
| 40 | cert.set_notAfter(b"20700101000000Z") |
Alex Schendel | 47af832 | 2023-07-21 13:32:38 -0700 | [diff] [blame] | 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 |
Ed Tanous | 7b9e256 | 2024-04-07 20:24:12 -0700 | [diff] [blame] | 66 | ), |
Alex Schendel | 47af832 | 2023-07-21 13:32:38 -0700 | [diff] [blame] | 67 | ] |
| 68 | ) |
| 69 | |
| 70 | # sign CA cert with CA key |
| 71 | cert.sign(key, "sha256") |
| 72 | return key, cert |
| 73 | |
| 74 | |
Ed Tanous | 7b9e256 | 2024-04-07 20:24:12 -0700 | [diff] [blame] | 75 | def 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 | |
| 115 | def generateCert(commonName, extensions, caKey, caCert, serial, csr=None): |
Alex Schendel | 47af832 | 2023-07-21 13:32:38 -0700 | [diff] [blame] | 116 | # key |
Alex Schendel | 47af832 | 2023-07-21 13:32:38 -0700 | [diff] [blame] | 117 | |
| 118 | # cert |
| 119 | cert = crypto.X509() |
Alex Schendel | 47af832 | 2023-07-21 13:32:38 -0700 | [diff] [blame] | 120 | cert.set_serial_number(serial) |
| 121 | cert.set_version(2) |
Ed Tanous | 7b9e256 | 2024-04-07 20:24:12 -0700 | [diff] [blame] | 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") |
Alex Schendel | 47af832 | 2023-07-21 13:32:38 -0700 | [diff] [blame] | 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 | |
Ed Tanous | 7b9e256 | 2024-04-07 20:24:12 -0700 | [diff] [blame] | 144 | extensions.extend( |
Alex Schendel | 47af832 | 2023-07-21 13:32:38 -0700 | [diff] [blame] | 145 | [ |
| 146 | crypto.X509Extension( |
| 147 | b"authorityKeyIdentifier", False, b"keyid", issuer=caCert |
Ed Tanous | 7b9e256 | 2024-04-07 20:24:12 -0700 | [diff] [blame] | 148 | ), |
Alex Schendel | 47af832 | 2023-07-21 13:32:38 -0700 | [diff] [blame] | 149 | ] |
| 150 | ) |
Ed Tanous | 7b9e256 | 2024-04-07 20:24:12 -0700 | [diff] [blame] | 151 | cert.add_extensions(extensions) |
Alex Schendel | 47af832 | 2023-07-21 13:32:38 -0700 | [diff] [blame] | 152 | cert.sign(caKey, "sha256") |
| 153 | return key, cert |
| 154 | |
| 155 | |
| 156 | def 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 Tanous | 7b9e256 | 2024-04-07 20:24:12 -0700 | [diff] [blame] | 177 | if "//" not in host: |
| 178 | host = f"https://{host}" |
| 179 | url = urllib.parse.urlparse(host, scheme="https") |
Alex Schendel | 47af832 | 2023-07-21 13:32:38 -0700 | [diff] [blame] | 180 | |
Ed Tanous | 7b9e256 | 2024-04-07 20:24:12 -0700 | [diff] [blame] | 181 | serial = 1000 |
| 182 | certsDir = os.path.join(SCRIPT_DIR, "certs") |
| 183 | print(f"Writing certs to {certsDir}") |
Alex Schendel | 47af832 | 2023-07-21 13:32:38 -0700 | [diff] [blame] | 184 | try: |
| 185 | print("Making certs directory.") |
Ed Tanous | 7b9e256 | 2024-04-07 20:24:12 -0700 | [diff] [blame] | 186 | os.mkdir(certsDir) |
Alex Schendel | 47af832 | 2023-07-21 13:32:38 -0700 | [diff] [blame] | 187 | except OSError as error: |
| 188 | if error.errno == 17: |
| 189 | print("certs directory already exists. Skipping...") |
| 190 | else: |
| 191 | print(error) |
Ed Tanous | 7b9e256 | 2024-04-07 20:24:12 -0700 | [diff] [blame] | 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.") |
Alex Schendel | 47af832 | 2023-07-21 13:32:38 -0700 | [diff] [blame] | 213 | serial += 1 |
Alex Schendel | 47af832 | 2023-07-21 13:32:38 -0700 | [diff] [blame] | 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 | ] |
Alex Schendel | 47af832 | 2023-07-21 13:32:38 -0700 | [diff] [blame] | 224 | |
Alex Schendel | 47af832 | 2023-07-21 13:32:38 -0700 | [diff] [blame] | 225 | redfishObject = redfish.redfish_client( |
Ed Tanous | 7b9e256 | 2024-04-07 20:24:12 -0700 | [diff] [blame] | 226 | base_url="https://" + url.netloc, |
Alex Schendel | 47af832 | 2023-07-21 13:32:38 -0700 | [diff] [blame] | 227 | username=username, |
| 228 | password=password, |
| 229 | default_prefix="/redfish/v1", |
| 230 | ) |
| 231 | redfishObject.login(auth="session") |
Ed Tanous | 7b9e256 | 2024-04-07 20:24:12 -0700 | [diff] [blame] | 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 | |
Alex Schendel | 47af832 | 2023-07-21 13:32:38 -0700 | [diff] [blame] | 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 | ) |
Ed Tanous | 7b9e256 | 2024-04-07 20:24:12 -0700 | [diff] [blame] | 341 | caCertJSON["CertificateUri"] = { |
| 342 | "@odata.id": caCertPath + "/1", |
| 343 | } |
| 344 | |
Alex Schendel | 47af832 | 2023-07-21 13:32:38 -0700 | [diff] [blame] | 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) |
Ed Tanous | 7b9e256 | 2024-04-07 20:24:12 -0700 | [diff] [blame] | 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 | |
Alex Schendel | 47af832 | 2023-07-21 13:32:38 -0700 | [diff] [blame] | 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( |
Ed Tanous | 7b9e256 | 2024-04-07 20:24:12 -0700 | [diff] [blame] | 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 | ), |
Alex Schendel | 47af832 | 2023-07-21 13:32:38 -0700 | [diff] [blame] | 391 | ) |
| 392 | response.raise_for_status() |
| 393 | print("Redfish TLS authentication success!") |
Alex Schendel | 47af832 | 2023-07-21 13:32:38 -0700 | [diff] [blame] | 394 | |
| 395 | |
Ed Tanous | 7b9e256 | 2024-04-07 20:24:12 -0700 | [diff] [blame] | 396 | if __name__ == "__main__": |
| 397 | main() |