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["@odata.id"] = ( 235 "/redfish/v1/Managers/bmc/NetworkProtocol/HTTPS/Certificates/1" 236 ) 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