-
-
Save dmerner/26b61d5d7cd67753110eb63b83d67e90 to your computer and use it in GitHub Desktop.
Supermicro IPMI certificate updater
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env python3 | |
# vim: autoindent tabstop=4 shiftwidth=4 expandtab softtabstop=4 filetype=python | |
# This file is part of Supermicro IPMI certificate updater. | |
# Supermicro IPMI certificate updater is free software: you can | |
# redistribute it and/or modify it under the terms of the GNU General Public | |
# License as published by the Free Software Foundation, version 2. | |
# | |
# This program is distributed in the hope that it will be useful, but WITHOUT | |
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS | |
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more | |
# details. | |
# | |
# You should have received a copy of the GNU General Public License along with | |
# this program; if not, write to the Free Software Foundation, Inc., 51 | |
# Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. | |
# | |
# Copyright (c) Jari Turkia | |
import os | |
import argparse | |
import requests | |
import logging | |
from datetime import datetime | |
from lxml import etree | |
from urllib.parse import urlparse | |
from requests.auth import HTTPBasicAuth | |
REQUEST_TIMEOUT = 5.0 | |
LOGIN_URL = '%s/cgi/login.cgi' | |
IPMI_CERT_INFO_URL = '%s/cgi/ipmi.cgi' | |
UPLOAD_CERT_URL = '%s/cgi/upload_ssl.cgi' | |
REBOOT_IPMI_URL = '%s/cgi/BMCReset.cgi' | |
CONFIG_CERT_URL = '%s/cgi/url_redirect.cgi?url_name=config_ssl' | |
REDFISH_ROOT = '%s/redfish/v1/' | |
REDFISH_SSL = '%sUpdateService/SSLCert/' | |
REDFISH_SSL_UPLOAD = '%sUpdateService/SSLCert/Actions/SSLCert.Upload' | |
REDFISH_BMC_REBOOT = '%sManagers/1/Actions/Manager.Reset' | |
def login(session, url, username, password): | |
""" | |
Log into IPMI interface | |
:param session: Current session object | |
:type session requests.session | |
:param url: base-URL to IPMI | |
:param username: username to use for logging in | |
:param password: password to use for logging in | |
:return: bool | |
""" | |
login_data = { | |
'name': username, | |
'pwd': password | |
} | |
login_url = LOGIN_URL % url | |
try: | |
result = session.post(login_url, login_data, timeout=REQUEST_TIMEOUT, verify=False) | |
except ConnectionError: | |
return False | |
if not result.ok: | |
return False | |
if '/cgi/url_redirect.cgi?url_name=mainmenu' not in result.text: | |
return False | |
return True | |
def get_ipmi_cert_info(session, url, model, user, password): | |
""" | |
Verify existing certificate information | |
:param session: Current session object | |
:type session requests.session | |
:param url: base-URL to IPMI | |
:return: dict | |
""" | |
timestamp = datetime.utcnow().strftime('%a %d %b %Y %H:%M:%S GMT') | |
if model == "X11": | |
try: | |
r = session.get(REDFISH_SSL % (REDFISH_ROOT % url), auth=HTTPBasicAuth(user, password), verify=False) | |
except ConnectionError: | |
return False | |
if not r.ok: | |
return False | |
data = r.json() | |
return { | |
'has_cert': True, | |
'valid_from': data['VaildFrom'], # Yes, Supermicro made a typo in their BMC API. | |
'valid_until': data['GoodTHRU'] | |
} | |
cert_info_data = { | |
'SSL_STATUS.XML': '(0,0)', | |
'time_stamp': timestamp # 'Thu Jul 12 2018 19:52:48 GMT+0300 (FLE Daylight Time)' | |
} | |
#for cookie in session.cookies: | |
# print(cookie) | |
ipmi_info_url = IPMI_CERT_INFO_URL % url | |
try: | |
result = session.post(ipmi_info_url, cert_info_data, timeout=REQUEST_TIMEOUT, verify=False) | |
except ConnectionError: | |
return False | |
if not result.ok: | |
return False | |
root = etree.fromstring(result.text) | |
# <?xml> <IPMI> <SSL_INFO> <STATUS> | |
status = root.xpath('//IPMI/SSL_INFO/STATUS') | |
if not status: | |
return False | |
# Since xpath will return a list, just pick the first one from it. | |
status = status[0] | |
has_cert = int(status.get('CERT_EXIST')) | |
has_cert = bool(has_cert) | |
if has_cert: | |
valid_from = status.get('VALID_FROM') | |
valid_until = status.get('VALID_UNTIL') | |
return { | |
'has_cert': has_cert, | |
'valid_from': valid_from, | |
'valid_until': valid_until | |
} | |
def get_ipmi_cert_valid(session, url): | |
""" | |
Verify existing certificate information | |
:param session: Current session object | |
:type session requests.session | |
:param url: base-URL to IPMI | |
:return: bool | |
""" | |
timestamp = datetime.utcnow().strftime('%a %d %b %Y %H:%M:%S GMT') | |
cert_info_data = { | |
'SSL_VALIDATE.XML': '(0,0)', | |
'time_stamp': timestamp # 'Thu Jul 12 2018 19:52:48 GMT+0300 (FLE Daylight Time)' | |
} | |
#for cookie in session.cookies: | |
# print(cookie) | |
ipmi_info_url = IPMI_CERT_INFO_URL % url | |
try: | |
result = session.post(ipmi_info_url, cert_info_data, timeout=REQUEST_TIMEOUT, verify=False) | |
except ConnectionError: | |
return False | |
if not result.ok: | |
return False | |
root = etree.fromstring(result.text) | |
# <?xml> <IPMI> <SSL_INFO> | |
status = root.xpath('//IPMI/SSL_INFO') | |
if not status: | |
return False | |
# Since xpath will return a list, just pick the first one from it. | |
status = status[0] | |
valid_cert = int(status.get('VALIDATE')) | |
return bool(valid_cert) | |
def upload_cert(session, url, key_file, cert_file, model, user, password): | |
""" | |
Send X.509 certificate and private key to server | |
:param session: Current session object | |
:type session requests.session | |
:param url: base-URL to IPMI | |
:param key_file: filename to X.509 certificate private key | |
:param cert_file: filename to X.509 certificate PEM | |
:return: | |
""" | |
with open(key_file, 'rb') as filehandle: | |
key_data = filehandle.read() | |
with open(cert_file, 'rb') as filehandle: | |
cert_data = filehandle.read() | |
if model == "X10": | |
files_to_upload = [ | |
('/tmp/cert.pem', ('cert.pem', cert_data, 'application/octet-stream')), | |
('/tmp/key.pem', ('key.pem', key_data, 'application/octet-stream')) | |
] | |
upload_cert_url = UPLOAD_CERT_URL % url | |
elif model == "X11": | |
upload_cert_url = REDFISH_SSL_UPLOAD % (REDFISH_ROOT % url) | |
files_to_upload = { | |
'cert_file' : ('fullchain.pem', open(cert_file, 'rb')), | |
'key_file' : ('privkey.pem', open(key_file, 'rb')) | |
} | |
try: | |
result = session.post(upload_cert_url, files=files_to_upload, timeout=REQUEST_TIMEOUT, auth=HTTPBasicAuth(user, password) if model == "X11" else None, verify=False) | |
except ConnectionError: | |
return False | |
if not result.ok: | |
return False | |
if model == "X10": | |
if 'Content-Type' not in result.headers.keys() or result.headers['Content-Type'] != 'text/html': | |
# On failure, Content-Type will be 'text/plain' and 'Transfer-Encoding' is 'chunked' | |
return False | |
if 'CONFPAGE_RESET' not in result.text: | |
return False | |
return True | |
def reboot_ipmi(session, url, model, user, password): | |
timestamp = datetime.utcnow().strftime('%a %d %b %Y %H:%M:%S GMT') | |
reboot_data = { | |
'time_stamp': timestamp # 'Thu Jul 12 2018 19:52:48 GMT+0300 (FLE Daylight Time)' | |
} | |
if model == "X10": | |
reboot_url = REBOOT_IPMI_URL % url | |
elif model == "X11": | |
reboot_url = REDFISH_BMC_REBOOT % (REDFISH_ROOT % url) | |
try: | |
result = session.post(reboot_url, reboot_data, timeout=REQUEST_TIMEOUT, auth=HTTPBasicAuth(user, password) if model == "X11" else None, verify=False) | |
except ConnectionError: | |
return False | |
if not result.ok: | |
return False | |
if model == "X10": | |
if '<STATE CODE="OK"/>' not in result.text: | |
return False | |
return True | |
def determine_model(session, url): | |
redfish_url = REDFISH_ROOT % url | |
try: | |
r = session.get(redfish_url, timeout=REQUEST_TIMEOUT, verify=False) | |
except ConnectionError: | |
exit(2) | |
if not r.ok: | |
exit(2) | |
data = r.json() | |
# The UpdateService methods are only available on newer X11 based boards | |
if "UpdateService" in data: | |
return "X11" | |
else: | |
return "X10" | |
def main(): | |
parser = argparse.ArgumentParser(description='Update Supermicro IPMI SSL certificate') | |
parser.add_argument('--ipmi-url', required=True, | |
help='Supermicro IPMI 2.0 URL') | |
parser.add_argument('--key-file', required=True, | |
help='X.509 Private key filename') | |
parser.add_argument('--cert-file', required=True, | |
help='X.509 Certificate filename') | |
parser.add_argument('--username', required=True, | |
help='IPMI username with admin access') | |
parser.add_argument('--password', required=True, | |
help='IPMI user password') | |
parser.add_argument('--no-reboot', action='store_true', | |
help='The default is to reboot the IPMI after upload for the change to take effect.') | |
parser.add_argument('--quiet', action='store_true', | |
help='Do not output anything if successful') | |
args = parser.parse_args() | |
# Confirm args | |
if not os.path.isfile(args.key_file): | |
print("--key-file '%s' doesn't exist!" % args.key_file) | |
exit(2) | |
if not os.path.isfile(args.cert_file): | |
print("--cert-file '%s' doesn't exist!" % args.cert_file) | |
exit(2) | |
if args.ipmi_url[-1] == '/': | |
args.ipmi_url = args.ipmi_url[0:-1] | |
if not args.quiet: | |
# Enable reuest logging | |
logging.basicConfig() | |
logging.getLogger().setLevel(logging.DEBUG) | |
requests_log = logging.getLogger("requests.packages.urllib3") | |
requests_log.setLevel(logging.DEBUG) | |
requests_log.propagate = True | |
# Start the operation | |
requests.packages.urllib3.disable_warnings(requests.packages.urllib3.exceptions.InsecureRequestWarning) | |
session = requests.session() | |
# First determine if we are X10 or X11 | |
model = determine_model(session, args.ipmi_url) | |
if not args.quiet: | |
print("Board model is " + model) | |
if model == "X10": | |
if not login(session, args.ipmi_url, args.username, args.password): | |
print("Login failed. Cannot continue!") | |
exit(2) | |
# Set mandatory cookies: | |
url_parts = urlparse(args.ipmi_url) | |
# Cookie: langSetFlag=0; language=English; SID=<dynamic session ID here!>; mainpage=configuration; subpage=config_ssl | |
mandatory_cookies = { | |
'langSetFlag': '0', | |
'language': 'English', | |
'mainpage': 'configuration', | |
'subpage': 'config_ssl' | |
} | |
for cookie_name, cookie_value in mandatory_cookies.items(): | |
session.cookies.set(cookie_name, cookie_value, domain=url_parts.hostname) | |
cert_info = get_ipmi_cert_info(session, args.ipmi_url, model, args.username, args.password) | |
if not cert_info: | |
print("Failed to extract certificate information from IPMI!") | |
if model == "X11": | |
print("Try checking either your IPMI network config or your IPMI license!") | |
exit(2) | |
if not args.quiet and cert_info['has_cert']: | |
print("There exists a certificate, which is valid until: %s" % cert_info['valid_until']) | |
# Go upload! | |
if not upload_cert(session, args.ipmi_url, args.key_file, args.cert_file, model, args.username, args.password): | |
print("Failed to upload X.509 files to IPMI!") | |
exit(2) | |
# Redfish currently doesn't have a way to download the certificate on X11 boards | |
if model == "X10": | |
cert_valid = get_ipmi_cert_valid(session, args.ipmi_url) | |
if not cert_valid: | |
print("Uploads failed validation") | |
exit(2) | |
if not args.quiet: | |
print("Uploaded files ok.") | |
cert_info = get_ipmi_cert_info(session, args.ipmi_url, model, args.username, args.password) | |
if not cert_info: | |
print("Failed to extract certificate information from IPMI!") | |
if model == "X11": | |
print("Try checking either your IPMI network config or your IPMI license!") | |
exit(2) | |
if not args.quiet and cert_info['has_cert']: | |
print("After upload, there exists a certificate, which is valid until: %s" % cert_info['valid_until']) | |
if not args.no_reboot: | |
if not args.quiet: | |
print("Rebooting IPMI to apply changes.") | |
if not reboot_ipmi(session, args.ipmi_url, model, args.username, args.password): | |
print("Rebooting failed! Go reboot it manually?") | |
if not args.quiet: | |
print("All done!") | |
if __name__ == "__main__": | |
main() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
hey i saw your comment on NovaUI about your frustration with the source code obfuscation. I'm in the same boat, and it's driving me nuts. Ive been able to decode a good portion of it, and if your interested in collaborating to decode the rest, ping me on discord : kevdome3000#9311