diff --git a/bdisk/bSSL.py b/bdisk/bSSL.py index b1d8144..8a39b00 100755 --- a/bdisk/bSSL.py +++ b/bdisk/bSSL.py @@ -13,87 +13,182 @@ def verifyCert(cert, key, CA = None): try: chk.check_privatekey() except OpenSSL.SSL.Error: - exit(("{0}: Key does not match certificate!".format(datetime.datetime.now()))) + return(False) + exit(("{0}: {1} does not match {2}!".format(datetime.datetime.now(), key, cert))) else: - print("{0}: Key verified against certificate successfully.".format(datetime.datetime.now())) + print("{0}: {1} verified against {2} successfully.".format(datetime.datetime.now(), key, cert)) + return(True) # This is disabled because there doesn't seem to currently be any way # to actually verify certificates against a given CA. #if CA: # try: # magic stuff here -def sslCAKey(): - key = OpenSSL.crypto.PKey() - print("{0}: Generating SSL CA key...".format(datetime.datetime.now())) - key.generate_key(OpenSSL.crypto.TYPE_RSA, 4096) - #print OpenSSL.crypto.dump_privatekey(OpenSSL.crypto.FILETYPE_PEM, key) +def sslCAKey(conf): + # TODO: use path from conf, even if it doesn't exist? + # if it does, read it into a pkey object + keyfile = conf['ipxe']['ssl_cakey'] + if os.path.isfile(keyfile): + try: + key = OpenSSL.crypto.load_privatekey( + OpenSSL.crypto.FILETYPE_PEM, + open(keyfile).read()) + except: + exit('{0}: ERROR: It seems that {1} is not a proper PEM-encoded SSL key.'.format( + datetime.datetime.now(), + keyfile)) + else: + key = OpenSSL.crypto.PKey() + print("{0}: Generating SSL CA key...".format(datetime.datetime.now())) + key.generate_key(OpenSSL.crypto.TYPE_RSA, 4096) + with open(keyfile, 'wb') as f: + f.write(OpenSSL.crypto.dump_privatekey(OpenSSL.crypto.FILETYPE_PEM, key)) return(key) def sslCA(conf, key = None): + # NOTE: 'key' is a pkey OBJECT, not a file. + keyfile = conf['ipxe']['ssl_cakey'] + crtfile = conf['ipxe']['ssl_ca'] if not key: + if os.path.isfile(keyfile): + try: + key = OpenSSL.crypto.load_privatekey( + OpenSSL.crypto.FILETYPE_PEM, + open(keyfile).read()) + except: + exit('{0}: ERROR: It seems that {1} is not a proper PEM-encoded SSL key.'.format( + datetime.datetime.now(), + keyfile)) + else: + exit('{0}: ERROR: We need a key to generate a CA certificate!'.format( + datetime.datetime.now())) + if os.path.isfile(crtfile): try: - key = conf['ipxe']['ssl_cakey'] + ca = OpenSSL.crypto.load_certificate( + OpenSSL.crypto.FILETYPE_PEM, + open(crtfile).read()) except: - exit("{0}: Cannot find a valid CA Key to use.".format(datetime.datetime.now())) - domain = (re.sub('^(https?|ftp)://([a-z0-9.-]+)/?.*$', '\g<2>', - conf['ipxe']['uri'], - flags=re.IGNORECASE)).lower() - # http://www.pyopenssl.org/en/stable/api/crypto.html#pkey-objects - # http://docs.ganeti.org/ganeti/2.14/html/design-x509-ca.html - ca = OpenSSL.crypto.X509() - ca.set_version(3) - ca.set_serial_number(1) - ca.get_subject().CN = domain - ca.gmtime_adj_notBefore(0) - # valid for ROUGHLY 10 years. years(ish) * days * hours * mins * secs. - # the paramater is in seconds, which is why we need to multiply them all together. - ca.gmtime_adj_notAfter(10 * 365 * 24 * 60 * 60) - ca.set_issuer(ca.get_subject()) - ca.set_pubkey(key) - ca.add_extensions([ - OpenSSL.crypto.X509Extension("basicConstraints", - True, - "CA:TRUE, pathlen:0"), - OpenSSL.crypto.X509Extension("keyUsage", - True, - "keyCertSign, cRLSign"), - OpenSSL.crypto.X509Extension("subjectKeyIdentifier", - False, - "hash", - subject = ca),]) - ca.sign(key, "sha512") - #print OpenSSL.crypto.dump_certificate(OpenSSL.crypto.FILETYPE_PEM, ca) + exit('{0}: ERROR: It seems that {1} is not a proper PEM-encoded SSL certificate.'.format( + datetime.datetime.now(), + crtfile)) + else: + domain = (re.sub('^(https?|ftp)://([a-z0-9.-]+)/?.*$', '\g<2>', + conf['ipxe']['uri'], + flags=re.IGNORECASE)).lower() + # http://www.pyopenssl.org/en/stable/api/crypto.html#pkey-objects + # http://docs.ganeti.org/ganeti/2.14/html/design-x509-ca.html + ca = OpenSSL.crypto.X509() + ca.set_version(3) + ca.set_serial_number(1) + #ca.get_subject().CN = domain + ca.get_subject().CN = '{0} CA'.format(conf['bdisk']['name']) + ca.gmtime_adj_notBefore(0) + # valid for ROUGHLY 10 years. years(ish) * days * hours * mins * secs. + # the paramater is in seconds, which is why we need to multiply them all together. + ca.gmtime_adj_notAfter(10 * 365 * 24 * 60 * 60) + ca.set_issuer(ca.get_subject()) + ca.set_pubkey(key) + ca.add_extensions([ + OpenSSL.crypto.X509Extension(b"basicConstraints", + True, + b"CA:TRUE, pathlen:0"), + OpenSSL.crypto.X509Extension(b"keyUsage", + True, + b"keyCertSign, cRLSign"), + OpenSSL.crypto.X509Extension(b"subjectKeyIdentifier", + False, + b"hash", + subject = ca),]) + ca.sign(key, "sha512") + with open(crtfile, 'wb') as f: + f.write(OpenSSL.crypto.dump_certificate(OpenSSL.crypto.FILETYPE_PEM, ca)) return(ca) -def sslCKey(): - key = OpenSSL.crypto.PKey() - print("{0}: Generating SSL Client key...".format(datetime.datetime.now())) - key.generate_key(OpenSSL.crypto.TYPE_RSA, 4096) - #print OpenSSL.crypto.dump_privatekey(OpenSSL.crypto.FILETYPE_PEM, key) +def sslCKey(conf): + keyfile = conf['ipxe']['ssl_key'] + if os.path.isfile(keyfile): + try: + key = OpenSSL.crypto.load_privatekey( + OpenSSL.crypto.FILETYPE_PEM, + open(keyfile).read()) + except: + exit('{0}: ERROR: It seems that {1} is not a proper PEM-encoded SSL key.'.format( + datetime.datetime.now(), + keyfile)) + else: + key = OpenSSL.crypto.PKey() + print("{0}: Generating SSL Client key...".format(datetime.datetime.now())) + key.generate_key(OpenSSL.crypto.TYPE_RSA, 4096) + with open(keyfile, 'wb') as f: + f.write(OpenSSL.crypto.dump_privatekey(OpenSSL.crypto.FILETYPE_PEM, key)) return(key) -def sslCSR(conf, key): +def sslCSR(conf, key = None): + # NOTE: 'key' is a pkey OBJECT, not a file. + keyfile = conf['ipxe']['ssl_key'] + crtfile = conf['ipxe']['ssl_crt'] + if not key: + if os.path.isfile(keyfile): + try: + key = OpenSSL.crypto.load_privatekey( + OpenSSL.crypto.FILETYPE_PEM, + open(keyfile).read()) + except: + exit('{0}: ERROR: It seems that {1} is not a proper PEM-encoded SSL key.'.format( + datetime.datetime.now(), + keyfile)) + else: + exit('{0}: ERROR: We need a key to generate a CSR!'.format( + datetime.datetime.now())) domain = (re.sub('^(https?|ftp)://([a-z0-9.-]+)/?.*$', '\g<2>', - conf['ipxe']['uri'], - flags=re.IGNORECASE)).lower() + conf['ipxe']['uri'], + flags=re.IGNORECASE)).lower() csr = OpenSSL.crypto.X509Req() csr.get_subject().CN = domain + #req.get_subject().countryName = 'xxx' + #req.get_subject().stateOrProvinceName = 'xxx' + #req.get_subject().localityName = 'xxx' + #req.get_subject().organizationName = 'xxx' + #req.get_subject().organizationalUnitName = 'xxx' csr.set_pubkey(key) csr.sign(key, "sha512") - #print OpenSSL.crypto.dump_certificate_request(OpenSSL.crypto.FILETYPE_PEM, req) + with open('/tmp/main.csr', 'wb') as f: + f.write(OpenSSL.crypto.dump_certificate_request(OpenSSL.crypto.FILETYPE_PEM, csr)) return(csr) -def sslSign(ca, key, csr): - ca_cert = OpenSSL.crypto.load_certificate(ca) - ca_key = OpenSSL.crypto.load_privatekey(key) - req = OpenSSL.crypto.load_certificate_request(csr) +def sslSign(conf, ca, key, csr): + #ca_cert = OpenSSL.crypto.load_certificate(OpenSSL.crypto.FILETYPE_PEM, ca) + #ca_key = OpenSSL.crypto.load_privatekey(key) + #req = OpenSSL.crypto.load_certificate_request(csr) + csr = OpenSSL.crypto.load_certificate_request(OpenSSL.crypto.FILETYPE_PEM, + open("/tmp/main.csr").read()) cert = OpenSSL.crypto.X509() - cert.set_subject(req.get_subject()) + cert.set_subject(csr.get_subject()) cert.set_serial_number(1) cert.gmtime_adj_notBefore(0) cert.gmtime_adj_notAfter(24 * 60 * 60) - cert.set_issuer(ca_cert.get_subject()) - cert.set_pubkey(req.get_pubkey()) - cert.sign(ca_key, "sha512") - #print OpenSSL.crypto.dump_certificate(OpenSSL.crypto.FILETYPE_PEM, cert) + cert.set_issuer(ca.get_subject()) + cert.set_pubkey(csr.get_pubkey()) + #cert.set_pubkey(ca.get_pubkey()) + cert.sign(key, "sha512") + with open(conf['ipxe']['ssl_crt'], 'wb') as f: + f.write(OpenSSL.crypto.dump_certificate(OpenSSL.crypto.FILETYPE_PEM, cert)) + return(cert) + +def sslPKI(conf): + # run checks for everything, gen what's missing + certfile = conf['ipxe']['ssl_crt'] + key = sslCAKey(conf) + ca = sslCA(conf, key = key) + ckey = sslCKey(conf) + if os.path.isfile(certfile): + cert = OpenSSL.crypto.load_certificate( + OpenSSL.crypto.FILETYPE_PEM, + open(certfile).read()) + if not verifyCert(cert, ckey): + csr = sslCSR(conf, ckey) + cert = sslSign(conf, ca, key, csr) + else: + csr = sslCSR(conf, ckey) + cert = sslSign(conf, ca, key, csr) return(cert) diff --git a/bdisk/bdisk.py b/bdisk/bdisk.py index 75dffdb..a8427ed 100755 --- a/bdisk/bdisk.py +++ b/bdisk/bdisk.py @@ -25,4 +25,6 @@ if __name__ == '__main__': build.genUEFI(conf['build'], conf['bdisk']) fulliso = build.genISO(conf) build.displayStats(fulliso) + if conf['build']['ipxe']: + bSSL.sslPKI(conf) print('{0}: Finish.'.format(datetime.datetime.now())) diff --git a/bdisk/host.py b/bdisk/host.py index 75338df..86a2131 100755 --- a/bdisk/host.py +++ b/bdisk/host.py @@ -30,9 +30,11 @@ def getConfig(conf_file='/etc/bdisk/build.ini'): default_conf_paths = ['/etc/bdisk/build.ini', '/usr/share/bdisk/build.ini', '/usr/share/bdisk/extra/build.ini', - '/usr/share/docs/bdisk/build.ini', + '/usr/share/docs/bdisk/build.ini', # this is the preferred installation path for packagers + '/usr/local/share/docs/bdisk/build.ini', '/opt/dev/bdisk/build.ini', - '/opt/dev/bdisk/extra/build.ini'] + '/opt/dev/bdisk/extra/build.ini', + '/opt/dev/bdisk/extra/dist.build.ini'] # if we weren't given one/using the default... if conf_file == '/etc/bdisk/build.ini': if not os.path.isfile(conf_file): @@ -42,6 +44,7 @@ def getConfig(conf_file='/etc/bdisk/build.ini'): break else: conf = conf_file + defconf = '{0}/../extra/dist.build.ini'.format(os.path.dirname(os.path.realpath(__file__))) if not conf: # okay, so let's check for distributed/"blank" ini's # since we can't seem to find one. @@ -50,13 +53,15 @@ def getConfig(conf_file='/etc/bdisk/build.ini'): if os.path.isfile(q): conf = q break - return(conf) + if os.path.isfile(default_conf_paths[4]): + defconf = default_conf_paths[4] + confs = [defconf, conf] + return(confs) -def parseConfig(conf): +def parseConfig(confs): config = configparser.ConfigParser() config._interpolation = configparser.ExtendedInterpolation() - config.read(conf) - bdisk_repo_dir = config['build']['basedir'] + config.read(confs) # a dict makes this so much easier. config_dict = {s:dict(config.items(s)) for s in config.sections()} # Convert the booleans to pythonic booleans in the dict... @@ -67,7 +72,7 @@ def parseConfig(conf): if config_dict['bdisk']['ver'] == '': repo = git.Repo(config_dict['build']['basedir']) refs = repo.git.describe(repo.head.commit).split('-') - config_dict['bdisk']['ver'] = refs[0] + '-' + refs[2] + config_dict['bdisk']['ver'] = refs[0] + 'r' + refs[2] for i in ('http', 'tftp', 'rsync', 'git'): config_dict['sync'][i] = config['sync'].getboolean(i) config_dict['ipxe']['iso'] = config['ipxe'].getboolean('iso') @@ -135,20 +140,4 @@ def parseConfig(conf): for x in ('http', 'tftp'): if config_dict['sync'][x]: os.makedirs(config_dict[x]['path'], exist_ok = True) - # Hoo boy. Now we test paths for SSL in iPXE... - if config_dict['build']['ipxe']: - if config_dict['ipxe']['ssl_crt']: - for x in ('ssl_key', 'ssl_cakey'): - if config_dict['ipxe'][x]: - if not os.path.isfile(config_dict['ipxe'][x]): - exit(('{0}: ERROR: {1} is not an existing file. Check your' + - 'configuration.').format( - datetime.datetime.now(), - config_dict['ipxe'][x])) - if config_dict['ipxe']['ssl_ca']: - if not os.path.isfile(config_dict['ipxe']['ssl_ca']): - exit(('{0}: ERROR: {1} is not an existing file. Check your' + - 'configuration.').format( - datetime.datetime.now(), - config_dict['ipxe']['ssl_ca'])) return(config, config_dict) diff --git a/docs/BDisk_User_Manual.v1.fodt b/docs/BDisk_User_Manual.v1.fodt index 4e71e3d..4f25b24 100644 --- a/docs/BDisk_User_Manual.v1.fodt +++ b/docs/BDisk_User_Manual.v1.fodt @@ -1,24 +1,24 @@ - 2016-12-01T11:27:37.6655108212016-12-04T05:22:38.498441678PT12H18M12S33LibreOffice/5.2.3.3$Linux_X86_64 LibreOffice_project/20m0$Build-3 + 2016-12-01T11:27:37.6655108212016-12-04T05:22:38.498441678PT12H18M12S33LibreOffice/5.2.3.3$Linux_X86_64 LibreOffice_project/20m0$Build-3 - 96203 + 93054 0 40748 - 20719 + 21751 true false view2 - 14942 - 106932 + 11578 + 3494 0 - 96203 + 93054 40746 - 116919 + 114803 0 1 false @@ -69,7 +69,7 @@ false false true - 947814 + 1037822 false false true @@ -345,114 +345,96 @@ - + + + + + + + + + + - + - + - + - + - + - + - + - + - + - + - + - + + + + - - - - + - - - - + - + - + + + + - - - - + - - - - - - - - + - - - - - - - - - - - - - - - - - @@ -668,13 +650,13 @@ - BDISK - Manual v1.0 + BDISK + Manual v1.0 Brent Saner bts@square-r00t.net - + Table of Contents @@ -761,87 +743,87 @@ - Table of Contents + Table of Contents - Chapter I: Introduction3 - Section I.1: What is BDisk?3 - Section I.2: Who wrote it?3 - Section I.3: What is this document?3 - I.3.i: Conventions used in this document3 - Section I.4: Further information/resources3 - I.4.i: For Users3 - I.4.ii: For Developers4 - Chapter II: Getting Started4 + Chapter I: Introduction3 + Section I.1: What is BDisk?3 + Section I.2: Who wrote it?3 + Section I.3: What is this document?3 + I.3.i: Conventions used in this document3 + Section I.4: Further information/resources3 + I.4.i: For Users3 + I.4.ii: For Developers4 + Chapter II: Getting Started4 - - Introduction - What is BDisk? + + Introduction + What is BDisk? BDisk refers to both a live distribution I use in my own uses (for rescue situations, recovery, etc.) but foremost and most importantly, it refers to the tool I use for building that distribution. This is what this project and documentation refer to when the word “BDisk” is used. BDisk is GPLv3-licensed. This means that you can use it for business reasons, personal reasons, modify it, etc. There are a few restrictions I retain, however, on this (don’t worry; they’re all in line with the GPLv3). You can find the full license in docs/LICENSE. When I rewrote BDisk in Python 3.x (I should take the time to note that I am still quite new to python so expect there to be plenty of optimizations to be made and general WTF-ery from seasoned python developers), one of my main goals was to make it as easy to use as possible. This is surprisingly hard to do- it’s quite challenging to try to approach software you’ve written with the mindset of someone other than you. Please see the For Users section (I.4.i). - Who wrote it? + Who wrote it? I (Brent Saner) am a GNU/Linux Systems/Network Administrator/Engineer- I wear a lot of hats. I have a lot of side projects to keep me busy when I’m not working at ${dayjob}, mostly to assist in other side projects and become more efficient and proficient at those tasks. “Shaving the yak,” indeed. I did a lot of research into how low-level boot operations take place, both in BIOS and UEFI1 - Unified Extensible Firmware Interface. UEFI is not BIOS, and BIOS is not UEFI. (and corresponding concepts such as Secureboot, etc.) which is no easy task to understand and very commonly misunderstood. (For instance, a common misconception is that UEFI necessarily implies Secureboot. This is quite far from the truth and UEFI by itself is quite a useful replacement for BIOS). Many of these misconceptions are simply due to lack of knowledge about the intricacies and complexities behind these technologies. Some of it is simply FUD2 - Fear, Uncertainty, Doubt- propaganda, in other words. generated to prey on the fears of those who don’t understand the underlying specifications or technology. + Unified Extensible Firmware Interface. UEFI is not BIOS, and BIOS is not UEFI. (and corresponding concepts such as Secureboot, etc.) which is no easy task to understand and very commonly misunderstood. (For instance, a common misconception is that UEFI necessarily implies Secureboot. This is quite far from the truth and UEFI by itself is quite a useful replacement for BIOS). Many of these misconceptions are simply due to lack of knowledge about the intricacies and complexities behind these technologies. Some of it is simply FUD2 + Fear, Uncertainty, Doubt- propaganda, in other words. generated to prey on the fears of those who don’t understand the underlying specifications or technology. It’s my hope that by releasing this utility and documenting it that you can use it and save some time for yourself as well (and hopefully get the chance to learn a bit more in the process!). - What is this document? + What is this document? This document is intended to be an indexed and easier-to-use reference than the other plaintext files (in docs/). Conventions used in this document - There are certain formats used in this document to specify what type of text they are representing. - + There are certain formats used in this document to specify what type of text they are representing. + - Commands will be in italics. + Commands will be in italics. - e.g. cat /tmp/file.txt + e.g. cat /tmp/file.txt - Paths (files, directories) will be in bold (unless part of a command, output, etc.). + Paths (files, directories) will be in bold (unless part of a command, output, etc.). - e.g. /tmp/file.txt + e.g. /tmp/file.txt - Variables will be underlined + Variables will be underlined - e.g. print(foo) + e.g. print(foo) - URLs (hyperlinks, really; you should be able to click on them) are bold and underlined. + URLs (hyperlinks, really; you should be able to click on them) are bold and underlined. - e.g. https://bdisk.square-r00t.net + e.g. https://bdisk.square-r00t.net - Paramaters/arguments will be either in <angled brackets>, [square brackets], or [<both>] + Paramaters/arguments will be either in <angled brackets>, [square brackets], or [<both>] - <> are used for positional arguments/parameters, or “placeholders” + <> are used for positional arguments/parameters, or “placeholders” - [] are used for optional arguments/parameters + [] are used for optional arguments/parameters - Thus e.g. someprog –dostuff <stufftodo> [--domorestuff <morestufftodo>] + Thus e.g. someprog –dostuff <stufftodo> [--domorestuff <morestufftodo>] - Further information/resources - For Users + Further information/resources + For Users If you encounter any bugs (or have any suggestions on how to improve BDisk!), please file a bug report in my bug tracker. - For Developers + For Developers The source is available to browse online or can be checked out via git (via the git protocol or http protocol). It is also available via Arch Linux’s AUR. If you are interested in packaging BDisk for other distributions, please feel free to contact me. Getting Started diff --git a/extra/dist.build.ini b/extra/dist.build.ini index d0baf84..7ec0786 100644 --- a/extra/dist.build.ini +++ b/extra/dist.build.ini @@ -403,6 +403,10 @@ usb = yes ; of curl. uri = https://bdisk.square-r00t.net +; Directory to hold SSL results, if we are generating +; keys, certificates, etc. +ssldir = ${build:dlpath}/ssl + ; Path to the (root) CA certificate file iPXE should use. ; Note that you can use your own CA to sign existing certs. ; See http://ipxe.org/crypto for more info. This is handy if @@ -411,39 +415,39 @@ uri = https://bdisk.square-r00t.net ; 0.) No whitespace ; 1.) Must be in PEM/X509 format ; 2.) REQUIRED if iso and/or usb is set to True/yes/etc. -; 3.) If specified, a matching key (ssl_cakey) MUST be +; 3.) If it exists, a matching key (ssl_cakey) MUST be ; specified -; 4.) HOWEVER, if left blank, one will be automatically -; generated -ssl_ca = +; 4.) HOWEVER, if left blank/doesn't exist, one will be +; automatically generated +ssl_ca = ${ssldir}/ca.crt ; Path to the (root) CA key file iPXE should use. ; 0.) No whitespace ; 1.) Must be in PEM/X509 format ; 2.) REQUIRED if iso and/or usb is set to True/yes/etc. -; 3.) If left blank (and ssl_ca is also blank), -; one will be automatically generated -; 4.) MUST match ssl_ca if specified +; 3.) If left blank or it doesn't exist (and ssl_ca is also +; blank), one will be automatically generated +; 4.) MUST match ssl_ca if specified/exists ; 5.) MUST NOT be passphrase-protected -ssl_cakey = +ssl_cakey = ${ssldir}/ca.key ; Path to the CLIENT certificate iPXE should use. ; 0.) No whitespace ; 1.) Must be in PEM/X509 format ; 2.) REQUIRED if iso and/or usb is set to True/yes/etc. -; 3.) If unspecified, a CA cert (ssl_ca) and key -; (ssl_cakey) MUST be specified -; 4.) HOWEVER, if left blank one will be generated +; 3.) If specified/existent, a matching CA cert (ssl_ca) +; and key (ssl_cakey) MUST be specified +; 4.) HOWEVER, if left blank/nonexistent, one will be generated ; 5.) MUST be signed by ssl_ca/ssl_ca if specified -ssl_crt = +ssl_crt = ${ssldir}/main.crt ; Path to the CLIENT key iPXE should use. ; 0.) No whitespace ; 1.) Must be in PEM/X509 format ; 2.) REQUIRED if iso and/or usb is set to True/yes/etc. -; 4.) If left blank (and ssl_ca is also blank), +; 4.) If left blank/nonexistent (and ssl_ca is also blank), ; one will be automatically generated -ssl_key = +ssl_key = ${ssldir}/main.key #---------------------------------------------------------#