From 47af83218a7f64b451b7e7bccc82ecdb75e68a0a Mon Sep 17 00:00:00 2001 From: Alex Schendel Date: Fri, 21 Jul 2023 13:32:38 -0700 Subject: scripts: Script to autogenerate TLS certs This script autogenerates: 1. Self-signed CA certificate/key pair 2. Server certificate/key pair 3. Client certificate/key pair 4. PKCS12 archive to store client certificate/key pair These files are all generated and then stored in a local ./certs directory. Following this, they are added to the BMC over Redfish. Then, the script attempts to use the client certificate/key pair to access a Redfish url with permissions while not providing username or password. If this succeeds, then it generates the PKCS12 archive file and directs the user to import it into a browser if they wish to test webui or would prefer to do any testing in browser rather than over curl or similar data tranfer tools for HTTP. Tested: Monitored output to ensure that each step succeeded and once the PKCS12 archive file was generated, imported it into a browser and accessed a redfish url with permissions while not being redirected to the login route. Change-Id: Ie8a393feb472281d1865e52bddbdb58edbf5b071 Signed-off-by: Alex Schendel --- scripts/generate_auth_certificates.py | 289 ++++++++++++++++++++++++++++++++++ 1 file changed, 289 insertions(+) create mode 100755 scripts/generate_auth_certificates.py (limited to 'scripts') diff --git a/scripts/generate_auth_certificates.py b/scripts/generate_auth_certificates.py new file mode 100755 index 0000000000..c05e381fae --- /dev/null +++ b/scripts/generate_auth_certificates.py @@ -0,0 +1,289 @@ +#!/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() -- cgit v1.2.3