confgen is done. messy, but done.

This commit is contained in:
brent s. 2018-05-21 05:58:25 -04:00
parent f4f131890d
commit 1d9b40a597
4 changed files with 246 additions and 103 deletions

View File

@ -625,7 +625,7 @@ class ConfGenerator(object):
'created that can be used to serve iPXE)'), 'created that can be used to serve iPXE)'),
'tftp': ('the TFTP directory (where a TFTP/' 'tftp': ('the TFTP directory (where a TFTP/'
'traditional PXE root is created)'), 'traditional PXE root is created)'),
'ssl': ('the SSL/TLS PKI directory (where we store ' 'pki': ('the SSL/TLS PKI directory (where we store '
'the PKI structure we use/re-use - MAKE SURE ' 'the PKI structure we use/re-use - MAKE SURE '
'it is in a path that is well-protected!)')} 'it is in a path that is well-protected!)')}
has_paths = False has_paths = False
@ -840,7 +840,7 @@ class ConfGenerator(object):
'then trying the built-in ~/.gnupg directory).' 'then trying the built-in ~/.gnupg directory).'
'\nGPG Home Directory: ')) '\nGPG Home Directory: '))
if _gpghome.strip() != '': if _gpghome.strip() != '':
gpg.attrib['gnupghome'] == _gpghome gpg.attrib['gnupghome'] = _gpghome
else: else:
_gpghome = 'none' _gpghome = 'none'
print('\n++ GPG || KEYSERVER PUSHING ++') print('\n++ GPG || KEYSERVER PUSHING ++')
@ -935,12 +935,12 @@ class ConfGenerator(object):
'\n\t'.join(_choices) '\n\t'.join(_choices)
))).strip().lower() ))).strip().lower()
if _export_type.startswith('a'): if _export_type.startswith('a'):
_export_type == 'asc' _export_type = 'asc'
elif _export_type.startswith('b'): elif _export_type.startswith('b'):
_export_type == 'bin' _export_type = 'bin'
else: else:
print('Using the default.') print('Using the default.')
_export_type == 'asc' _export_type = 'asc'
elem.attrib['format'] = _export_type elem.attrib['format'] = _export_type
_path = None _path = None
while not _path: while not _path:

116
bdisk/prompt_strings.py Normal file
View File

@ -0,0 +1,116 @@
# These are *key* ciphers, for encrypting exported keys.
openssl_ciphers = ['aes128', 'aes192', 'aes256', 'bf', 'blowfish',
'camellia128', 'camellia192', 'camellia256', 'cast', 'des',
'des3', 'idea', 'rc2', 'seed']
# These are *hash algorithms* for cert digests.
openssl_digests = ['blake2b512', 'blake2s256', 'gost', 'md4', 'md5', 'mdc2',
'rmd160', 'sha1', 'sha224', 'sha256', 'sha384', 'sha512']

class PromptStrings(object):
gpg = {
'attribs': {
'algo': {
'text': 'the subkey\'s encryption type/algorithm',
'choices': ['rsa', 'dsa'],
'default': 'rsa'
},
'keysize': {
'text': 'the subkey\'s key size (in bits)',
'choices': {
'rsa': ['1024', '2048', '4096'],
'dsa': ['768', '2048', '3072']
},
'default': {
'rsa': '4096',
'dsa': '3072'
}
}
},
'params': ['name', 'email', 'comment']
}
ssl = {
'attribs': {
'cert': {
'hash_algo': {
'text': ('What hashing algorithm do you want to use? '
'(Default is sha512.)'),
'prompt': 'Hashing algorithm: ',
'options': openssl_digests,
'default': 'aes256'
}
},
'key': {
'cipher': {
'text': ('What encryption algorithm/cipher do you want to '
'use? (Default is aes256.) Use "none" to specify '
'a key without a passphrase.'),
'prompt': 'Cipher: ',
'options': openssl_ciphers + ['none'],
'default': 'aes256'
},
'keysize': {
'text': ('What keysize/length (in bits) do you want the '
'key to be? (Default is 4096; much higher values '
'are possible but are untested and thus not '
'supported by this tool; feel free to edit the '
'generated configuration by hand.) (If the key '
'cipher is "none", this is ignored.)'),
'prompt': 'Keysize: ',
# TODO: do all openssl_ciphers support these sizes?
'options': ['1024', '2048', '4096'],
'default': 'aes256'
},
'passphrase': {
'text': ('What passphrase do you want to use for the key? '
'If you specified the cipher as "none", this is '
'ignored (you can just hit enter).'),
'prompt': 'Passphrase (will not echo back): ',
'options': None,
'default': ''
}
}
},
'paths': {
'cert': '(or read from) the certificate',
'key': '(or read from) the key',
'csr': ('(or read from) the certificate signing request (if '
'blank, we won\'t write to disk - the operation will '
'occur entirely in memory assuming we need to generate/'
'sign)')
},
'paths_ca': {
'index': ('(or read from) the CA (Certificate Authority) Database '
'index file (if left blank, one will not be used)'),
'serial': ('(or read from) the CA (Certificate Authority) '
'Database serial file (if left blank, one will not be '
'used)'),
},
'subject': {
'countryName': {
'text': ('the 2-letter country abbreviation (must conform to '
'ISO3166 ALPHA-2)?\n'
'Country code: ')
},
'localityName': {
'text': ('the city/town/borough/locality name?\n'
'Locality: ')
},
'stateOrProvinceName': {
'text': ('the state/region name (full string)?\n'
'Region: ')
},
'organization': {
'text': ('your organization\'s name?\n'
'Organization: ')
},
'organizationalUnitName': {
'text': ('your department/role/team/department name?\n'
'Organizational Unit: ')
},
'emailAddress': {
'text': ('the email address to be associated with this '
'certificate/PKI object?\n'
'Email: ')
}
}
}

View File

@ -1,11 +1,14 @@
import _io import _io
import copy
import crypt import crypt
import GPG import GPG
import getpass
import hashid import hashid
import hashlib import hashlib
import iso3166 import iso3166
import os import os
import pprint import pprint
import prompt_strings
import re import re
import string import string
import uuid import uuid
@ -33,12 +36,7 @@ crypt_map = {'sha512': crypt.METHOD_SHA512,
'md5': crypt.METHOD_MD5, 'md5': crypt.METHOD_MD5,
'des': crypt.METHOD_CRYPT} 'des': crypt.METHOD_CRYPT}


# These are *key* ciphers, for encrypting exported keys.
openssl_ciphers = ['aes128', 'aes192', 'aes256', 'bf', 'blowfish',
'camellia128', 'camellia192', 'camellia256', 'cast', 'des',
'des3', 'idea', 'rc2', 'seed']
openssl_digests = ['blake2b512', 'blake2s256', 'gost', 'md4', 'md5', 'mdc2',
'rmd160', 'sha1', 'sha224', 'sha256', 'sha384', 'sha512']


class XPathFmt(string.Formatter): class XPathFmt(string.Formatter):
def get_field(self, field_name, args, kwargs): def get_field(self, field_name, args, kwargs):
@ -162,7 +160,10 @@ class generate(object):


class prompts(object): class prompts(object):
def __init__(self): def __init__(self):
pass self.promptstr = prompt_strings.PromptStrings()

# TODO: these strings should be indexed in a separate module and
# sourced/imported. we should generally just find a cleaner way to do this.


def confirm_or_no(self, prompt = '', invert = False, def confirm_or_no(self, prompt = '', invert = False,
usage = '{0} to confirm, otherwise {1}...\n'): usage = '{0} to confirm, otherwise {1}...\n'):
@ -193,6 +194,62 @@ class prompts(object):
return(False) return(False)
return(True) return(True)


def gpg_keygen_attribs(self):
_strs = self.promptstr.gpg
gpg_vals = {'attribs': {},
'params': {}}
_checks = {
'params': {
'name': {'error': 'name cannot be empty',
'check': valid().nonempty_str},
'email': {'error': 'not a valid email address',
'check': valid().email}
}
}
for a in _strs['attribs']:
_a = None
while not _a:
if 'algo' in gpg_vals['attribs'] and a == 'keysize':
_algo = gpg_vals['attribs']['algo']
_choices = _strs['attribs']['keysize']['choices'][_algo]
_dflt = _strs['attribs']['keysize']['default'][_algo]
else:
_choices = _strs['attribs'][a]['choices']
_dflt = _strs['attribs'][a]['default']
_a = (input(
('\nWhat should be {0}? (Default is {1}.)\nChoices:\n'
'\n\t{2}\n\n{3}: ').format(
_strs['attribs'][a]['text'],
_dflt,
'\n\t'.join(_choices),
a.title()
)
)).strip().lower()
if _a == '':
_a = _dflt
elif _a not in _choices:
print(
('Invalid selection; choosing default '
'({0})').format(_dflt)
)
_a = _dflt
gpg_vals['attribs'][a] = _a
for p in _strs['params']:
_p = input(
('\nWhat is the {0} for the subkey?\n'
'{1}: ').format(p, p.title())
)
if p in _checks['params']:
while not _checks['params'][p]['check'](_p):
print(
('Invalid entry ({0}). Please retry.').format(
_checks['params'][p]['error']
)
)
_p = input('{0}: '.format(_p.title()))
gpg_vals['params'][p] = _p
return(gpg_vals)

def hash_select(self, prompt = '', def hash_select(self, prompt = '',
hash_types = generate().hashlib_names()): hash_types = generate().hashlib_names()):
_hash_types = hash_types _hash_types = hash_types
@ -240,116 +297,81 @@ class prompts(object):
ssl_vals = {'paths': {}, ssl_vals = {'paths': {},
'attribs': {}, 'attribs': {},
'subject': {}} 'subject': {}}
_checks = {
'subject': {
'countryName': valid().country_abbrev,
'emailAddress': valid().email
}
}
_strs = copy.deepcopy(self.promptstr.ssl)
# pki_role should be 'ca' or 'client' # pki_role should be 'ca' or 'client'
if pki_role not in ('ca', 'client'): if pki_role not in ('ca', 'client'):
raise ValueError('pki_role must be either "ca" or "client"') raise ValueError('pki_role must be either "ca" or "client"')
_attribs = {'cert': {'hash_algo': {'text': ('What hashing algorithm ' # NOTE: need to validate US and email
'do you want to use? (Default is sha512.)'),
'prompt': 'Hashing algorithm: ',
'options': openssl_digests,
'default': 'sha512'}},
'key': {'cipher': {'text': ('What encryption algorithm/'
'cipher do you want to use? (Default is '
'aes256.)'),
'prompt': 'Cipher: ',
'options': openssl_ciphers,
'default': 'aes256'},
# This can actually theoretically be anywhere from
# 512 to... who knows how high. I couldn't find the
# upper bound. So we just set it to sensible
# defaults. If they want something higher, they can
# edit the XML when they're done.
'keysize': {'text': ('What keysize/length (in '
'bits) do you want the key to be? (Default is '
'4096; much higher values are possible but '
'are untested and thus not supported by this '
'tool; feel free to edit the generated '
'configuration by hand.)'),
'prompt': 'Keysize: ',
'options': ['1024', '2048', '4096'],
'default': '4096'}}}
_paths = {'cert': '(or read from) the certificate',
'key': '(or read from) the key',
'csr': ('(or read from) the certificate signing request (if '
'blank, we won\'t write to disk - the operation '
'will occur entirely in memory assuming we need to '
'generate/sign)')}
if pki_role == 'ca': if pki_role == 'ca':
_paths['index'] = ('(or read from) the CA DB index file (if left ' # this is getting triggered for clients?
'blank, one will not be used)') _strs['paths'].update(_strs['paths_ca'])
_paths['serial'] = ('(or read from) the CA DB serial file (if ' for a in _strs['attribs']:
'left blank, one will not be used)')
for a in _attribs:
ssl_vals['attribs'][a] = {} ssl_vals['attribs'][a] = {}
for x in _attribs[a]: for x in _strs['attribs'][a]:
ssl_vals['attribs'][a][x] = None ssl_vals['attribs'][a][x] = None
for p in _paths: for p in _strs['paths']:
if p == 'csr': if p == 'csr':
_allow_empty = True _allow_empty = True
else: else:
_allow_empty = False _allow_empty = False
ssl_vals['paths'][p] = self.path(_paths[p], ssl_vals['paths'][p] = self.path(_strs['paths'][p],
empty_passthru = _allow_empty) empty_passthru = _allow_empty)
print() print()
if ssl_vals['paths'][p] == '': if ssl_vals['paths'][p] == '':
ssl_vals['paths'][p] = None ssl_vals['paths'][p] = None
if p in _attribs: if p in _strs['attribs']:
for x in _attribs[p]: for x in _strs['attribs'][p]:
while not ssl_vals['attribs'][p][x]: while not ssl_vals['attribs'][p][x]:
ssl_vals['attribs'][p][x] = (input( # cipher attrib is prompted for before this.
('\n{0}\n\n\t{1}\n\n{2}').format( if p == 'key' and x == 'passphrase':
_attribs[p][x]['text'], if ssl_vals['attribs']['key']['cipher'] == 'none':
'\n\t'.join(_attribs[p][x]['options']), ssl_vals['attribs'][p][x] = 'none'
_attribs[p][x]['prompt']) continue
)).strip().lower() ssl_vals['attribs'][p][x] = getpass.getpass(
if ssl_vals['attribs'][p][x] not in \ ('{0}\n{1}').format(
_attribs[p][x]['options']: _strs['attribs'][p][x]['text'],
print(('\nInvalid selection; setting default ' _strs['attribs'][p][x]['prompt'])
'({0}).').format(_attribs[p][x]['default'])) )
ssl_vals['attribs'][p][x] = \ if ssl_vals['attribs'][p][x] == '':
_attribs[p][x]['default'] ssl_vals['attribs'][p][x] = 'none'
_subject = {'countryName': {'text': ('the 2-letter country ' else:
'abbreviation (must conform to ' ssl_vals['attribs'][p][x] = (input(
'ISO3166 ALPHA-2)?\nCountry ' ('\n{0}\n\n\t{1}\n\n{2}').format(
'code: '), _strs['attribs'][p][x]['text'],
'check': 'func', '\n\t'.join(
'func': valid().country_abbrev}, _strs['attribs'][p][x]['options']),
'localityName': {'text': ('the city/town/borough/locality ' _strs['attribs'][p][x]['prompt']))
'name?\nLocality: '), ).strip().lower()
'check': None}, if ssl_vals['attribs'][p][x] not in \
'stateOrProvinceName': {'text': ('the state/region ' _strs['attribs'][p][x]['options']:
'name (full string)?' print(
'\nRegion: '), ('\nInvalid selection; setting default '
'check': None}, '({0}).').format(
'organization': {'text': ('your organization\'s name?' _strs['attribs'][p][x]['default']
'\nOrganization: '), )
'check': None}, )
'organizationalUnitName': {'text': ('your department/role/' ssl_vals['attribs'][p][x] = \
'team/department name?' _strs['attribs'][p][x]['default']
'\nOrganizational ' for s in _strs['subject']:
'Unit: '),
'check': None},
'emailAddress': {'text': ('the email address to be '
'associated with this '
'certificate/PKI object?\n'
'Email: '),
'check': 'func',
'func': valid().email}}
for s in _subject:
ssl_vals['subject'][s] = None ssl_vals['subject'][s] = None
for s in _subject: for s in _strs['subject']:
while not ssl_vals['subject'][s]: while not ssl_vals['subject'][s]:
_input = (input( _input = (input(
('\nWhat is {0}').format(_subject[s]['text']) ('\nWhat is {0}').format(
_strs['subject'][s]['text'])
)).strip() )).strip()
_chk = _subject[s]['check']
if _chk:
if _chk == 'func':
_chk = _subject[s]['func'](_input)
if not _chk:
print('Invalid value; retrying.')
continue
print() print()
if s in _checks['subject']:
if not _checks['subject'][s](_input):
print('Invalid entry; try again.')
ssl_vals['subject'][s] = None
continue
ssl_vals['subject'][s] = _input ssl_vals['subject'][s] = _input
_url = transform().url_to_dict(cn_url, no_None = True) _url = transform().url_to_dict(cn_url, no_None = True)
ssl_vals['subject']['commonName'] = _url['host'] ssl_vals['subject']['commonName'] = _url['host']
@ -594,6 +616,11 @@ class valid(object):
return(False) return(False)
return() return()


def nonempty_str(self, str_in):
if str_in.strip() == '':
return(False)
return(True)

def password(self, passwd): def password(self, passwd):
# https://en.wikipedia.org/wiki/ASCII#Printable_characters # https://en.wikipedia.org/wiki/ASCII#Printable_characters
# https://serverfault.com/a/513243/103116 # https://serverfault.com/a/513243/103116

View File

@ -156,7 +156,7 @@
publish="no" publish="no"
prompt_passphrase="no"> prompt_passphrase="no">
<!-- The below is only used if we are generating a key (i.e. keyid="none"). --> <!-- The below is only used if we are generating a key (i.e. keyid="none"). -->
<key type="rsa" keysize="4096" expire="0"> <key algo="rsa" keysize="4096" expire="0">
<name>{xpath%../../../../meta/dev/author/text()}</name> <name>{xpath%../../../../meta/dev/author/text()}</name>
<email>{xpath%../../../../meta/dev/email/text()}</email> <email>{xpath%../../../../meta/dev/email/text()}</email>
<comment>for {xpath%../../../../meta/names/pname/text()} [autogenerated] | {xpath%../../../../meta/uri/text()} | {xpath%../../../../meta/desc/text()}</comment> <comment>for {xpath%../../../../meta/names/pname/text()} [autogenerated] | {xpath%../../../../meta/uri/text()} | {xpath%../../../../meta/desc/text()}</comment>