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 <ed@tanous.net>
diff --git a/scripts/generate_auth_certificates.py b/scripts/generate_auth_certificates.py
index b5ae698..47e3592 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 @@
 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 @@
     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 @@
         [
             crypto.X509Extension(
                 b"authorityKeyIdentifier", False, b"keyid:always", issuer=cert
-            )
+            ),
         ]
     )
 
@@ -67,19 +72,65 @@
     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 @@
     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 @@
             '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 @@
         ),
         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,
+        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
+    )
+
+    clientKeyDump = crypto.dump_privatekey(crypto.FILETYPE_PEM, clientKey)
+    with open(os.path.join(certsDir, "client-key.pem"), "wb") as f:
+        f.write(clientKeyDump)
+        print("Client key generated.")
+    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",
+        ),
+        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(
+        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
+
+    serverCertDump = crypto.dump_certificate(crypto.FILETYPE_PEM, serverCert)
+
+    with open(os.path.join(certsDir, "server-cert.pem"), "wb") as f:
+        f.write(serverCertDump)
+        print("Server cert generated.")
+
+    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.")
+
     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 @@
         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 @@
     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()