From 7b9e256961fe529ce874cae572c17baa4fa0bbc3 Mon Sep 17 00:00:00 2001 From: Ed Tanous Date: Sun, 7 Apr 2024 20:24:12 -0700 Subject: Fix generate auth certs bmcs might not have the correct time, so allow certificates for 100 years starting from epoch. As is, the script makes the certificate valid for now + 10 years. After changes make the script valid from epoch (1970) to 100 years later (2070). This makes the script run to completion against a qemu instance of the bmc. Additional changes include detecting if a CA key is already present, to not rewrite it. This allows installing a CA certificate on test machines once, and using it to authenticate forever. Additionally, add "alternative names" support, for pointing to a bmc at localhost, or on the default qemu port, which allows these things to work by default in those scenarios. Lastly, change the directory to use a path relative to the script path, instead of relative to current path when generating certificates. This ensures that certs are always generated in the same place, which helps when a CA is reused. Tested: Script runs to completion without errors. Change-Id: Ia5c31041dd5cb193b897bf1f7bae3cd9767656d0 Signed-off-by: Ed Tanous --- scripts/generate_auth_certificates.py | 284 +++++++++++++++++++++++----------- 1 file changed, 196 insertions(+), 88 deletions(-) (limited to 'scripts') diff --git a/scripts/generate_auth_certificates.py b/scripts/generate_auth_certificates.py index b5ae698476..47e3592f8e 100755 --- a/scripts/generate_auth_certificates.py +++ b/scripts/generate_auth_certificates.py @@ -2,6 +2,8 @@ import argparse import os +import socket +import urllib import requests @@ -14,6 +16,8 @@ try: except ImportError: raise Exception("Please run pip install pyOpenSSL to run this script.") +SCRIPT_DIR = os.path.dirname(os.path.realpath(__file__)) + # 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 @@ -31,8 +35,9 @@ def generateCACert(serial): 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) + + cert.set_notBefore(b"19700101000000Z") + cert.set_notAfter(b"20700101000000Z") caCertSubject = cert.get_subject() caCertSubject.countryName = "US" @@ -58,7 +63,7 @@ def generateCACert(serial): [ crypto.X509Extension( b"authorityKeyIdentifier", False, b"keyid:always", issuer=cert - ) + ), ] ) @@ -67,19 +72,65 @@ def generateCACert(serial): return key, cert -def generateCert(commonName, extensions, caKey, caCert, serial): +def generateCertCsr( + redfishObject, commonName, extensions, caKey, caCert, serial +): + + try: + socket.inet_aton(commonName) + commonName = "IP: " + commonName + except socket.error: + commonName = "DNS: " + commonName + + CSRRequest = { + "CommonName": commonName, + "City": "San Fransisco", + "Country": "US", + "Organization": "", + "OrganizationalUnit": "", + "State": "CA", + "CertificateCollection": { + "@odata.id": "/redfish/v1/Managers/bmc/NetworkProtocol/HTTPS/Certificates", + }, + "AlternativeNames": [ + commonName, + "DNS: localhost", + "IP: 127.0.0.1", + ], + } + + response = redfishObject.post( + "/redfish/v1/CertificateService/Actions/CertificateService.GenerateCSR", + body=CSRRequest, + ) + + if response.status != 200: + raise Exception("Failed to create CSR") + + csrString = response.dict["CSRString"] + + return crypto.load_certificate_request(crypto.FILETYPE_PEM, csrString) + + +def generateCert(commonName, extensions, caKey, caCert, serial, csr=None): # 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) + + if csr is None: + key = crypto.PKey() + key.generate_key(crypto.TYPE_RSA, 2048) + cert.set_pubkey(key) + else: + key = None + cert.set_subject(csr.get_subject()) + cert.set_pubkey(csr.get_pubkey()) + + cert.set_notBefore(b"19700101000000Z") + cert.set_notAfter(b"20700101000000Z") certSubject = cert.get_subject() certSubject.countryName = "US" @@ -90,15 +141,14 @@ def generateCert(commonName, extensions, caKey, caCert, serial): certSubject.commonName = commonName cert.set_issuer(caCert.get_issuer()) - cert.add_extensions(extensions) - cert.add_extensions( + extensions.extend( [ crypto.X509Extension( b"authorityKeyIdentifier", False, b"keyid", issuer=caCert - ) + ), ] ) - + cert.add_extensions(extensions) cert.sign(caKey, "sha256") return key, cert @@ -124,26 +174,43 @@ def main(): '0penBmc'. Use --username and --password flags to change these, respectively.""" ) - serial = 1000 + if "//" not in host: + host = f"https://{host}" + url = urllib.parse.urlparse(host, scheme="https") + serial = 1000 + certsDir = os.path.join(SCRIPT_DIR, "certs") + print(f"Writing certs to {certsDir}") try: print("Making certs directory.") - os.mkdir("certs") + os.mkdir(certsDir) except OSError as error: if error.errno == 17: print("certs directory already exists. Skipping...") else: print(error) - caKey, caCert = generateCACert(serial) + + cacertFilename = os.path.join(certsDir, "CA-cert.cer") + cakeyFilename = os.path.join(certsDir, "CA-key.pem") + if os.path.exists(cacertFilename): + with open(cacertFilename, "rb") as cacert_file: + caCertDump = cacert_file.read() + caCert = crypto.load_certificate(crypto.FILETYPE_PEM, caCertDump) + with open(cakeyFilename, "rb") as cakey_file: + caKeyDump = cakey_file.read() + caKey = crypto.load_privatekey(crypto.FILETYPE_PEM, caKeyDump) + else: + + caKey, caCert = generateCACert(serial) + caKeyDump = crypto.dump_privatekey(crypto.FILETYPE_PEM, caKey) + caCertDump = crypto.dump_certificate(crypto.FILETYPE_PEM, caCert) + with open(cacertFilename, "wb") as f: + f.write(caCertDump) + print("CA cert generated.") + with open(cakeyFilename, "wb") as f: + f.write(caKeyDump) + print("CA key generated.") 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( @@ -154,64 +221,127 @@ def main(): ), crypto.X509Extension(b"extendedKeyUsage", True, b"clientAuth"), ] + + redfishObject = redfish.redfish_client( + base_url="https://" + url.netloc, + username=username, + password=password, + default_prefix="/redfish/v1", + ) + redfishObject.login(auth="session") + 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: + with open(os.path.join(certsDir, "client-key.pem"), "wb") as f: f.write(clientKeyDump) print("Client key generated.") - with open("certs/client-cert.pem", "wb") as f: + serial += 1 + clientCertDump = crypto.dump_certificate(crypto.FILETYPE_PEM, clientCert) + + with open(os.path.join(certsDir, "client-cert.pem"), "wb") as f: f.write(clientCertDump) print("Client cert generated.") + san_list = [ + b"DNS: localhost", + b"IP: 127.0.0.1", + ] + + try: + socket.inet_aton(url.hostname) + san_list.append(b"IP: " + url.hostname.encode()) + except socket.error: + san_list.append(b"DNS: " + url.hostname.encode()) + serverExtensions = [ crypto.X509Extension( b"keyUsage", True, - b"""digitalSignature, - keyAgreement""", + b"digitalSignature, keyAgreement", ), crypto.X509Extension(b"extendedKeyUsage", True, b"serverAuth"), + crypto.X509Extension(b"subjectAltName", False, b", ".join(san_list)), ] + + useCSR = True + + if useCSR: + csr = generateCertCsr( + redfishObject, + url.hostname, + serverExtensions, + caKey, + caCert, + serial, + ) + serverKey = None + serverKeyDumpStr = "" + else: + csr = None serverKey, serverCert = generateCert( - host, serverExtensions, caKey, caCert, serial + url.hostname, serverExtensions, caKey, caCert, serial, csr=csr ) + if serverKey is not None: + serverKeyDump = crypto.dump_privatekey(crypto.FILETYPE_PEM, serverKey) + with open(os.path.join(certsDir, "server-key.pem"), "wb") as f: + f.write(serverKeyDump) + print("Server key generated.") + serverKeyDumpStr = serverKeyDump.decode() 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: + + with open(os.path.join(certsDir, "server-cert.pem"), "wb") as f: f.write(serverCertDump) print("Server cert generated.") - caCertJSON = {} - caCertJSON["CertificateString"] = caCertDump.decode() - caCertJSON["CertificateType"] = "PEM" + serverCertDumpStr = serverCertDump.decode() + + print("Generating p12 cert file for browser authentication.") + pkcs12Cert = crypto.PKCS12() + pkcs12Cert.set_certificate(clientCert) + if clientKey: + pkcs12Cert.set_privatekey(clientKey) + pkcs12Cert.set_ca_certificates([caCert]) + pkcs12Cert.set_friendlyname(bytes(username, encoding="utf-8")) + with open(os.path.join(certsDir, "client.p12"), "wb") as f: + f.write(pkcs12Cert.export()) + print("Client p12 cert file generated and stored in 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." + ) + + caCertJSON = { + "CertificateString": caCertDump.decode(), + "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 + caCertJSON["CertificateUri"] = { + "@odata.id": caCertPath + "/1", + } + response = redfishObject.post(replaceCertPath, body=caCertJSON) if response.status == 200: print("Successfully replaced existing CA certificate.") @@ -226,16 +356,14 @@ def main(): 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" + serverCertJSON = { + "CertificateString": serverKeyDumpStr + serverCertDumpStr, + "CertificateUri": { + "@odata.id": "/redfish/v1/Managers/bmc/NetworkProtocol/HTTPS/Certificates/1", + }, + "CertificateType": "PEM", + } + print("Replacing server certificate...") response = redfishObject.post(replaceCertPath, body=serverCertJSON) if response.status == 200: @@ -254,36 +382,16 @@ def main(): 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"), + f"https://{url.netloc}/redfish/v1/SessionService/Sessions", + verify=os.path.join(certsDir, "CA-cert.cer"), + cert=( + os.path.join(certsDir, "client-cert.pem"), + os.path.join(certsDir, "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() +if __name__ == "__main__": + main() -- cgit v1.2.3