| #!/usr/bin/env python3 |
| |
| import argparse |
| import os |
| |
| import requests |
| |
| try: |
| import redfish |
| except ModuleNotFoundError: |
| raise Exception("Please run pip install redfish to run this script.") |
| try: |
| from OpenSSL import crypto |
| except ImportError: |
| raise Exception("Please run pip install pyOpenSSL to run this script.") |
| |
| # Script to generate a certificates for a CA, server, and client |
| # allowing for client authentication using mTLS certificates. |
| # This can then be used to test mTLS client authentication for Redfish |
| # and webUI. Note that this requires the pyOpenSSL library to function. |
| # TODO: Use EC keys rather than RSA keys. |
| |
| |
| def generateCACert(serial): |
| # CA key |
| key = crypto.PKey() |
| key.generate_key(crypto.TYPE_RSA, 2048) |
| |
| # CA cert |
| cert = crypto.X509() |
| cert.set_serial_number(serial) |
| cert.set_version(2) |
| cert.set_pubkey(key) |
| cert.gmtime_adj_notBefore(0) |
| cert.gmtime_adj_notAfter(10 * 365 * 24 * 60 * 60) |
| |
| caCertSubject = cert.get_subject() |
| caCertSubject.countryName = "US" |
| caCertSubject.stateOrProvinceName = "California" |
| caCertSubject.localityName = "San Francisco" |
| caCertSubject.organizationName = "OpenBMC" |
| caCertSubject.organizationalUnitName = "bmcweb" |
| caCertSubject.commonName = "Test CA" |
| cert.set_issuer(caCertSubject) |
| |
| cert.add_extensions( |
| [ |
| crypto.X509Extension( |
| b"basicConstraints", True, b"CA:TRUE, pathlen:0" |
| ), |
| crypto.X509Extension(b"keyUsage", True, b"keyCertSign, cRLSign"), |
| crypto.X509Extension( |
| b"subjectKeyIdentifier", False, b"hash", subject=cert |
| ), |
| ] |
| ) |
| cert.add_extensions( |
| [ |
| crypto.X509Extension( |
| b"authorityKeyIdentifier", False, b"keyid:always", issuer=cert |
| ) |
| ] |
| ) |
| |
| # sign CA cert with CA key |
| cert.sign(key, "sha256") |
| return key, cert |
| |
| |
| def generateCert(commonName, extensions, caKey, caCert, serial): |
| # key |
| key = crypto.PKey() |
| key.generate_key(crypto.TYPE_RSA, 2048) |
| |
| # cert |
| cert = crypto.X509() |
| serial |
| cert.set_serial_number(serial) |
| cert.set_version(2) |
| cert.set_pubkey(key) |
| cert.gmtime_adj_notBefore(0) |
| cert.gmtime_adj_notAfter(365 * 24 * 60 * 60) |
| |
| certSubject = cert.get_subject() |
| certSubject.countryName = "US" |
| certSubject.stateOrProvinceName = "California" |
| certSubject.localityName = "San Francisco" |
| certSubject.organizationName = "OpenBMC" |
| certSubject.organizationalUnitName = "bmcweb" |
| certSubject.commonName = commonName |
| cert.set_issuer(caCert.get_issuer()) |
| |
| cert.add_extensions(extensions) |
| cert.add_extensions( |
| [ |
| crypto.X509Extension( |
| b"authorityKeyIdentifier", False, b"keyid", issuer=caCert |
| ) |
| ] |
| ) |
| |
| cert.sign(caKey, "sha256") |
| return key, cert |
| |
| |
| def main(): |
| parser = argparse.ArgumentParser() |
| parser.add_argument("--host", help="Host to connect to", required=True) |
| parser.add_argument( |
| "--username", help="Username to connect with", default="root" |
| ) |
| parser.add_argument( |
| "--password", |
| help="Password for user in order to install certs over Redfish.", |
| default="0penBmc", |
| ) |
| args = parser.parse_args() |
| host = args.host |
| username = args.username |
| password = args.password |
| if username == "root" and password == "0penBMC": |
| print( |
| """Note: Using default username 'root' and default password |
| '0penBmc'. Use --username and --password flags to change these, |
| respectively.""" |
| ) |
| serial = 1000 |
| |
| try: |
| print("Making certs directory.") |
| os.mkdir("certs") |
| except OSError as error: |
| if error.errno == 17: |
| print("certs directory already exists. Skipping...") |
| else: |
| print(error) |
| caKey, caCert = generateCACert(serial) |
| serial += 1 |
| caKeyDump = crypto.dump_privatekey(crypto.FILETYPE_PEM, caKey) |
| caCertDump = crypto.dump_certificate(crypto.FILETYPE_PEM, caCert) |
| with open("certs/CA-cert.pem", "wb") as f: |
| f.write(caCertDump) |
| print("CA cert generated.") |
| with open("certs/CA-key.pem", "wb") as f: |
| f.write(caKeyDump) |
| print("CA key generated.") |
| |
| clientExtensions = [ |
| crypto.X509Extension( |
| b"keyUsage", |
| True, |
| b"""digitalSignature, |
| keyAgreement""", |
| ), |
| crypto.X509Extension(b"extendedKeyUsage", True, b"clientAuth"), |
| ] |
| clientKey, clientCert = generateCert( |
| username, clientExtensions, caKey, caCert, serial |
| ) |
| serial += 1 |
| clientKeyDump = crypto.dump_privatekey(crypto.FILETYPE_PEM, clientKey) |
| clientCertDump = crypto.dump_certificate(crypto.FILETYPE_PEM, clientCert) |
| with open("certs/client-key.pem", "wb") as f: |
| f.write(clientKeyDump) |
| print("Client key generated.") |
| with open("certs/client-cert.pem", "wb") as f: |
| f.write(clientCertDump) |
| print("Client cert generated.") |
| |
| serverExtensions = [ |
| crypto.X509Extension( |
| b"keyUsage", |
| True, |
| b"""digitalSignature, |
| keyAgreement""", |
| ), |
| crypto.X509Extension(b"extendedKeyUsage", True, b"serverAuth"), |
| ] |
| serverKey, serverCert = generateCert( |
| host, serverExtensions, caKey, caCert, serial |
| ) |
| serial += 1 |
| serverKeyDump = crypto.dump_privatekey(crypto.FILETYPE_PEM, serverKey) |
| serverCertDump = crypto.dump_certificate(crypto.FILETYPE_PEM, serverCert) |
| with open("certs/server-key.pem", "wb") as f: |
| f.write(serverKeyDump) |
| print("Server key generated.") |
| with open("certs/server-cert.pem", "wb") as f: |
| f.write(serverCertDump) |
| print("Server cert generated.") |
| |
| caCertJSON = {} |
| caCertJSON["CertificateString"] = caCertDump.decode() |
| caCertJSON["CertificateType"] = "PEM" |
| caCertPath = "/redfish/v1/Managers/bmc/Truststore/Certificates" |
| replaceCertPath = "/redfish/v1/CertificateService/Actions/" |
| replaceCertPath += "CertificateService.ReplaceCertificate" |
| print("Attempting to install CA certificate to BMC.") |
| redfishObject = redfish.redfish_client( |
| base_url="https://" + host, |
| username=username, |
| password=password, |
| default_prefix="/redfish/v1", |
| ) |
| redfishObject.login(auth="session") |
| response = redfishObject.post(caCertPath, body=caCertJSON) |
| if response.status == 500: |
| print( |
| "An existing CA certificate is likely already installed." |
| " Replacing..." |
| ) |
| caCertificateUri = {} |
| caCertificateUri["@odata.id"] = caCertPath + "/1" |
| caCertJSON["CertificateUri"] = caCertificateUri |
| response = redfishObject.post(replaceCertPath, body=caCertJSON) |
| if response.status == 200: |
| print("Successfully replaced existing CA certificate.") |
| else: |
| raise Exception( |
| "Could not install or replace CA certificate." |
| "Please check if a certificate is already installed. If a" |
| "certificate is already installed, try performing a factory" |
| "restore to clear such settings." |
| ) |
| elif response.status == 200: |
| print("Successfully installed CA certificate.") |
| else: |
| raise Exception("Could not install certificate: " + response.read) |
| serverCertJSON = {} |
| serverCertJSON["CertificateString"] = ( |
| serverKeyDump.decode() + serverCertDump.decode() |
| ) |
| serverCertificateUri = {} |
| serverCertificateUri["@odata.id"] = ( |
| "/redfish/v1/Managers/bmc/NetworkProtocol/HTTPS/Certificates/1" |
| ) |
| serverCertJSON["CertificateUri"] = serverCertificateUri |
| serverCertJSON["CertificateType"] = "PEM" |
| print("Replacing server certificate...") |
| response = redfishObject.post(replaceCertPath, body=serverCertJSON) |
| if response.status == 200: |
| print("Successfully replaced server certificate.") |
| else: |
| raise Exception("Could not replace certificate: " + response.read) |
| tlsPatchJSON = {"Oem": {"OpenBMC": {"AuthMethods": {"TLS": True}}}} |
| print("Ensuring TLS authentication is enabled.") |
| response = redfishObject.patch( |
| "/redfish/v1/AccountService", body=tlsPatchJSON |
| ) |
| if response.status == 200: |
| print("Successfully enabled TLS authentication.") |
| else: |
| raise Exception("Could not enable TLS auth: " + response.read) |
| redfishObject.logout() |
| print("Testing redfish TLS authentication with generated certs.") |
| response = requests.get( |
| "https://" + host + "/redfish/v1/SessionService/Sessions", |
| verify=False, |
| cert=("certs/client-cert.pem", "certs/client-key.pem"), |
| ) |
| response.raise_for_status() |
| print("Redfish TLS authentication success!") |
| print("Generating p12 cert file for browser authentication.") |
| pkcs12Cert = crypto.PKCS12() |
| pkcs12Cert.set_certificate(clientCert) |
| pkcs12Cert.set_privatekey(clientKey) |
| pkcs12Cert.set_ca_certificates([caCert]) |
| pkcs12Cert.set_friendlyname(bytes(username, encoding="utf-8")) |
| with open("certs/client.p12", "wb") as f: |
| f.write(pkcs12Cert.export()) |
| print( |
| "Client p12 cert file generated and stored in" |
| "./certs/client.p12." |
| ) |
| print( |
| "Copy this file to a system with a browser and install the" |
| "cert into the browser." |
| ) |
| print( |
| "You will then be able to test redfish and webui" |
| "authentication using this certificate." |
| ) |
| print( |
| "Note: this p12 file was generated without a password, so it" |
| "can be imported easily." |
| ) |
| |
| |
| main() |