1#!/usr/bin/env python3
2
3import argparse
4import os
5
6import requests
7
8try:
9    import redfish
10except ModuleNotFoundError:
11    raise Exception("Please run pip install redfish to run this script.")
12try:
13    from OpenSSL import crypto
14except ImportError:
15    raise Exception("Please run pip install pyOpenSSL to run this script.")
16
17# Script to generate a certificates for a CA, server, and client
18# allowing for client authentication using mTLS certificates.
19# This can then be used to test mTLS client authentication for Redfish
20# and webUI. Note that this requires the pyOpenSSL library to function.
21# TODO: Use EC keys rather than RSA keys.
22
23
24def generateCACert(serial):
25    # CA key
26    key = crypto.PKey()
27    key.generate_key(crypto.TYPE_RSA, 2048)
28
29    # CA cert
30    cert = crypto.X509()
31    cert.set_serial_number(serial)
32    cert.set_version(2)
33    cert.set_pubkey(key)
34    cert.gmtime_adj_notBefore(0)
35    cert.gmtime_adj_notAfter(10 * 365 * 24 * 60 * 60)
36
37    caCertSubject = cert.get_subject()
38    caCertSubject.countryName = "US"
39    caCertSubject.stateOrProvinceName = "California"
40    caCertSubject.localityName = "San Francisco"
41    caCertSubject.organizationName = "OpenBMC"
42    caCertSubject.organizationalUnitName = "bmcweb"
43    caCertSubject.commonName = "Test CA"
44    cert.set_issuer(caCertSubject)
45
46    cert.add_extensions(
47        [
48            crypto.X509Extension(
49                b"basicConstraints", True, b"CA:TRUE, pathlen:0"
50            ),
51            crypto.X509Extension(b"keyUsage", True, b"keyCertSign, cRLSign"),
52            crypto.X509Extension(
53                b"subjectKeyIdentifier", False, b"hash", subject=cert
54            ),
55        ]
56    )
57    cert.add_extensions(
58        [
59            crypto.X509Extension(
60                b"authorityKeyIdentifier", False, b"keyid:always", issuer=cert
61            )
62        ]
63    )
64
65    # sign CA cert with CA key
66    cert.sign(key, "sha256")
67    return key, cert
68
69
70def generateCert(commonName, extensions, caKey, caCert, serial):
71    # key
72    key = crypto.PKey()
73    key.generate_key(crypto.TYPE_RSA, 2048)
74
75    # cert
76    cert = crypto.X509()
77    serial
78    cert.set_serial_number(serial)
79    cert.set_version(2)
80    cert.set_pubkey(key)
81    cert.gmtime_adj_notBefore(0)
82    cert.gmtime_adj_notAfter(365 * 24 * 60 * 60)
83
84    certSubject = cert.get_subject()
85    certSubject.countryName = "US"
86    certSubject.stateOrProvinceName = "California"
87    certSubject.localityName = "San Francisco"
88    certSubject.organizationName = "OpenBMC"
89    certSubject.organizationalUnitName = "bmcweb"
90    certSubject.commonName = commonName
91    cert.set_issuer(caCert.get_issuer())
92
93    cert.add_extensions(extensions)
94    cert.add_extensions(
95        [
96            crypto.X509Extension(
97                b"authorityKeyIdentifier", False, b"keyid", issuer=caCert
98            )
99        ]
100    )
101
102    cert.sign(caKey, "sha256")
103    return key, cert
104
105
106def main():
107    parser = argparse.ArgumentParser()
108    parser.add_argument("--host", help="Host to connect to", required=True)
109    parser.add_argument(
110        "--username", help="Username to connect with", default="root"
111    )
112    parser.add_argument(
113        "--password",
114        help="Password for user in order to install certs over Redfish.",
115        default="0penBmc",
116    )
117    args = parser.parse_args()
118    host = args.host
119    username = args.username
120    password = args.password
121    if username == "root" and password == "0penBMC":
122        print(
123            """Note: Using default username 'root' and default password
124            '0penBmc'. Use --username and --password flags to change these,
125            respectively."""
126        )
127    serial = 1000
128
129    try:
130        print("Making certs directory.")
131        os.mkdir("certs")
132    except OSError as error:
133        if error.errno == 17:
134            print("certs directory already exists. Skipping...")
135        else:
136            print(error)
137    caKey, caCert = generateCACert(serial)
138    serial += 1
139    caKeyDump = crypto.dump_privatekey(crypto.FILETYPE_PEM, caKey)
140    caCertDump = crypto.dump_certificate(crypto.FILETYPE_PEM, caCert)
141    with open("certs/CA-cert.pem", "wb") as f:
142        f.write(caCertDump)
143        print("CA cert generated.")
144    with open("certs/CA-key.pem", "wb") as f:
145        f.write(caKeyDump)
146        print("CA key generated.")
147
148    clientExtensions = [
149        crypto.X509Extension(
150            b"keyUsage",
151            True,
152            b"""digitalSignature,
153                             keyAgreement""",
154        ),
155        crypto.X509Extension(b"extendedKeyUsage", True, b"clientAuth"),
156    ]
157    clientKey, clientCert = generateCert(
158        username, clientExtensions, caKey, caCert, serial
159    )
160    serial += 1
161    clientKeyDump = crypto.dump_privatekey(crypto.FILETYPE_PEM, clientKey)
162    clientCertDump = crypto.dump_certificate(crypto.FILETYPE_PEM, clientCert)
163    with open("certs/client-key.pem", "wb") as f:
164        f.write(clientKeyDump)
165        print("Client key generated.")
166    with open("certs/client-cert.pem", "wb") as f:
167        f.write(clientCertDump)
168        print("Client cert generated.")
169
170    serverExtensions = [
171        crypto.X509Extension(
172            b"keyUsage",
173            True,
174            b"""digitalSignature,
175                             keyAgreement""",
176        ),
177        crypto.X509Extension(b"extendedKeyUsage", True, b"serverAuth"),
178    ]
179    serverKey, serverCert = generateCert(
180        host, serverExtensions, caKey, caCert, serial
181    )
182    serial += 1
183    serverKeyDump = crypto.dump_privatekey(crypto.FILETYPE_PEM, serverKey)
184    serverCertDump = crypto.dump_certificate(crypto.FILETYPE_PEM, serverCert)
185    with open("certs/server-key.pem", "wb") as f:
186        f.write(serverKeyDump)
187        print("Server key generated.")
188    with open("certs/server-cert.pem", "wb") as f:
189        f.write(serverCertDump)
190        print("Server cert generated.")
191
192    caCertJSON = {}
193    caCertJSON["CertificateString"] = caCertDump.decode()
194    caCertJSON["CertificateType"] = "PEM"
195    caCertPath = "/redfish/v1/Managers/bmc/Truststore/Certificates"
196    replaceCertPath = "/redfish/v1/CertificateService/Actions/"
197    replaceCertPath += "CertificateService.ReplaceCertificate"
198    print("Attempting to install CA certificate to BMC.")
199    redfishObject = redfish.redfish_client(
200        base_url="https://" + host,
201        username=username,
202        password=password,
203        default_prefix="/redfish/v1",
204    )
205    redfishObject.login(auth="session")
206    response = redfishObject.post(caCertPath, body=caCertJSON)
207    if response.status == 500:
208        print(
209            "An existing CA certificate is likely already installed."
210            " Replacing..."
211        )
212        caCertificateUri = {}
213        caCertificateUri["@odata.id"] = caCertPath + "/1"
214        caCertJSON["CertificateUri"] = caCertificateUri
215        response = redfishObject.post(replaceCertPath, body=caCertJSON)
216        if response.status == 200:
217            print("Successfully replaced existing CA certificate.")
218        else:
219            raise Exception(
220                "Could not install or replace CA certificate."
221                "Please check if a certificate is already installed. If a"
222                "certificate is already installed, try performing a factory"
223                "restore to clear such settings."
224            )
225    elif response.status == 200:
226        print("Successfully installed CA certificate.")
227    else:
228        raise Exception("Could not install certificate: " + response.read)
229    serverCertJSON = {}
230    serverCertJSON["CertificateString"] = (
231        serverKeyDump.decode() + serverCertDump.decode()
232    )
233    serverCertificateUri = {}
234    serverCertificateUri[
235        "@odata.id"
236    ] = "/redfish/v1/Managers/bmc/NetworkProtocol/HTTPS/Certificates/1"
237    serverCertJSON["CertificateUri"] = serverCertificateUri
238    serverCertJSON["CertificateType"] = "PEM"
239    print("Replacing server certificate...")
240    response = redfishObject.post(replaceCertPath, body=serverCertJSON)
241    if response.status == 200:
242        print("Successfully replaced server certificate.")
243    else:
244        raise Exception("Could not replace certificate: " + response.read)
245    tlsPatchJSON = {"Oem": {"OpenBMC": {"AuthMethods": {"TLS": True}}}}
246    print("Ensuring TLS authentication is enabled.")
247    response = redfishObject.patch(
248        "/redfish/v1/AccountService", body=tlsPatchJSON
249    )
250    if response.status == 200:
251        print("Successfully enabled TLS authentication.")
252    else:
253        raise Exception("Could not enable TLS auth: " + response.read)
254    redfishObject.logout()
255    print("Testing redfish TLS authentication with generated certs.")
256    response = requests.get(
257        "https://" + host + "/redfish/v1/SessionService/Sessions",
258        verify=False,
259        cert=("certs/client-cert.pem", "certs/client-key.pem"),
260    )
261    response.raise_for_status()
262    print("Redfish TLS authentication success!")
263    print("Generating p12 cert file for browser authentication.")
264    pkcs12Cert = crypto.PKCS12()
265    pkcs12Cert.set_certificate(clientCert)
266    pkcs12Cert.set_privatekey(clientKey)
267    pkcs12Cert.set_ca_certificates([caCert])
268    pkcs12Cert.set_friendlyname(bytes(username, encoding="utf-8"))
269    with open("certs/client.p12", "wb") as f:
270        f.write(pkcs12Cert.export())
271        print(
272            "Client p12 cert file generated and stored in"
273            "./certs/client.p12."
274        )
275        print(
276            "Copy this file to a system with a browser and install the"
277            "cert into the browser."
278        )
279        print(
280            "You will then be able to test redfish and webui"
281            "authentication using this certificate."
282        )
283        print(
284            "Note: this p12 file was generated without a password, so it"
285            "can be imported easily."
286        )
287
288
289main()
290