Alex Schendel | 47af832 | 2023-07-21 13:32:38 -0700 | [diff] [blame] | 1 | #!/usr/bin/env python3 |
| 2 | |
| 3 | import argparse |
| 4 | import os |
| 5 | |
| 6 | import requests |
| 7 | |
| 8 | try: |
| 9 | import redfish |
| 10 | except ModuleNotFoundError: |
| 11 | raise Exception("Please run pip install redfish to run this script.") |
| 12 | try: |
| 13 | from OpenSSL import crypto |
| 14 | except 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 | |
| 24 | def 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 | |
| 70 | def 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 | |
| 106 | def 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 = {} |
Ed Tanous | 4c8c12d | 2024-01-28 22:25:50 -0800 | [diff] [blame] | 234 | serverCertificateUri["@odata.id"] = ( |
| 235 | "/redfish/v1/Managers/bmc/NetworkProtocol/HTTPS/Certificates/1" |
| 236 | ) |
Alex Schendel | 47af832 | 2023-07-21 13:32:38 -0700 | [diff] [blame] | 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 | |
| 289 | main() |