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 <alex.schendel@intel.com>
diff --git a/scripts/generate_auth_certificates.py b/scripts/generate_auth_certificates.py
new file mode 100755
index 0000000..c05e381
--- /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()