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)'),
'tftp': ('the TFTP directory (where a TFTP/'
'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 '
'it is in a path that is well-protected!)')}
has_paths = False
@ -840,7 +840,7 @@ class ConfGenerator(object):
'then trying the built-in ~/.gnupg directory).'
'\nGPG Home Directory: '))
if _gpghome.strip() != '':
gpg.attrib['gnupghome'] == _gpghome
gpg.attrib['gnupghome'] = _gpghome
else:
_gpghome = 'none'
print('\n++ GPG || KEYSERVER PUSHING ++')
@ -935,12 +935,12 @@ class ConfGenerator(object):
'\n\t'.join(_choices)
))).strip().lower()
if _export_type.startswith('a'):
_export_type == 'asc'
_export_type = 'asc'
elif _export_type.startswith('b'):
_export_type == 'bin'
_export_type = 'bin'
else:
print('Using the default.')
_export_type == 'asc'
_export_type = 'asc'
elem.attrib['format'] = _export_type
_path = None
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 copy
import crypt
import GPG
import getpass
import hashid
import hashlib
import iso3166
import os
import pprint
import prompt_strings
import re
import string
import uuid
@ -33,12 +36,7 @@ crypt_map = {'sha512': crypt.METHOD_SHA512,
'md5': crypt.METHOD_MD5,
'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):
def get_field(self, field_name, args, kwargs):
@ -162,7 +160,10 @@ class generate(object):

class prompts(object):
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,
usage = '{0} to confirm, otherwise {1}...\n'):
@ -193,6 +194,62 @@ class prompts(object):
return(False)
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 = '',
hash_types = generate().hashlib_names()):
_hash_types = hash_types
@ -240,116 +297,81 @@ class prompts(object):
ssl_vals = {'paths': {},
'attribs': {},
'subject': {}}
_checks = {
'subject': {
'countryName': valid().country_abbrev,
'emailAddress': valid().email
}
}
_strs = copy.deepcopy(self.promptstr.ssl)
# pki_role should be 'ca' or 'client'
if pki_role not in ('ca', 'client'):
raise ValueError('pki_role must be either "ca" or "client"')
_attribs = {'cert': {'hash_algo': {'text': ('What hashing algorithm '
'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)')}
# NOTE: need to validate US and email
if pki_role == 'ca':
_paths['index'] = ('(or read from) the CA DB index file (if left '
'blank, one will not be used)')
_paths['serial'] = ('(or read from) the CA DB serial file (if '
'left blank, one will not be used)')
for a in _attribs:
# this is getting triggered for clients?
_strs['paths'].update(_strs['paths_ca'])
for a in _strs['attribs']:
ssl_vals['attribs'][a] = {}
for x in _attribs[a]:
for x in _strs['attribs'][a]:
ssl_vals['attribs'][a][x] = None
for p in _paths:
for p in _strs['paths']:
if p == 'csr':
_allow_empty = True
else:
_allow_empty = False
ssl_vals['paths'][p] = self.path(_paths[p],
ssl_vals['paths'][p] = self.path(_strs['paths'][p],
empty_passthru = _allow_empty)
print()
if ssl_vals['paths'][p] == '':
ssl_vals['paths'][p] = None
if p in _attribs:
for x in _attribs[p]:
if p in _strs['attribs']:
for x in _strs['attribs'][p]:
while not ssl_vals['attribs'][p][x]:
# cipher attrib is prompted for before this.
if p == 'key' and x == 'passphrase':
if ssl_vals['attribs']['key']['cipher'] == 'none':
ssl_vals['attribs'][p][x] = 'none'
continue
ssl_vals['attribs'][p][x] = getpass.getpass(
('{0}\n{1}').format(
_strs['attribs'][p][x]['text'],
_strs['attribs'][p][x]['prompt'])
)
if ssl_vals['attribs'][p][x] == '':
ssl_vals['attribs'][p][x] = 'none'
else:
ssl_vals['attribs'][p][x] = (input(
('\n{0}\n\n\t{1}\n\n{2}').format(
_attribs[p][x]['text'],
'\n\t'.join(_attribs[p][x]['options']),
_attribs[p][x]['prompt'])
)).strip().lower()
_strs['attribs'][p][x]['text'],
'\n\t'.join(
_strs['attribs'][p][x]['options']),
_strs['attribs'][p][x]['prompt']))
).strip().lower()
if ssl_vals['attribs'][p][x] not in \
_attribs[p][x]['options']:
print(('\nInvalid selection; setting default '
'({0}).').format(_attribs[p][x]['default']))
_strs['attribs'][p][x]['options']:
print(
('\nInvalid selection; setting default '
'({0}).').format(
_strs['attribs'][p][x]['default']
)
)
ssl_vals['attribs'][p][x] = \
_attribs[p][x]['default']
_subject = {'countryName': {'text': ('the 2-letter country '
'abbreviation (must conform to '
'ISO3166 ALPHA-2)?\nCountry '
'code: '),
'check': 'func',
'func': valid().country_abbrev},
'localityName': {'text': ('the city/town/borough/locality '
'name?\nLocality: '),
'check': None},
'stateOrProvinceName': {'text': ('the state/region '
'name (full string)?'
'\nRegion: '),
'check': None},
'organization': {'text': ('your organization\'s name?'
'\nOrganization: '),
'check': None},
'organizationalUnitName': {'text': ('your department/role/'
'team/department name?'
'\nOrganizational '
'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:
_strs['attribs'][p][x]['default']
for s in _strs['subject']:
ssl_vals['subject'][s] = None
for s in _subject:
for s in _strs['subject']:
while not ssl_vals['subject'][s]:
_input = (input(
('\nWhat is {0}').format(_subject[s]['text'])
('\nWhat is {0}').format(
_strs['subject'][s]['text'])
)).strip()
_chk = _subject[s]['check']
if _chk:
if _chk == 'func':
_chk = _subject[s]['func'](_input)
if not _chk:
print('Invalid value; retrying.')
continue
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
_url = transform().url_to_dict(cn_url, no_None = True)
ssl_vals['subject']['commonName'] = _url['host']
@ -594,6 +616,11 @@ class valid(object):
return(False)
return()

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

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

View File

@ -156,7 +156,7 @@
publish="no"
prompt_passphrase="no">
<!-- 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>
<email>{xpath%../../../../meta/dev/email/text()}</email>
<comment>for {xpath%../../../../meta/names/pname/text()} [autogenerated] | {xpath%../../../../meta/uri/text()} | {xpath%../../../../meta/desc/text()}</comment>