""":mod:`geofront.regen` --- Regen master key ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ .. versionadded:: 0.2.0 """ import argparse import logging import os.path from typing import Mapping, Optional, Tuple, Type from paramiko.pkey import PKey from paramiko.rsakey import RSAKey from typeguard import typechecked from .keystore import get_key_fingerprint from .masterkey import (EmptyStoreError, KeyGenerationError, MasterKeyStore, generate_key, renew_master_key) from .remote import RemoteSet from .version import VERSION __all__ = 'main', 'main_parser', 'get_regen_options', 'regenerate' @typechecked def main_parser( parser: argparse.ArgumentParser=None ) -> argparse.ArgumentParser: # pragma: no cover """Create an :class:`~argparse.ArgumentParser` object for :program:`geofront-key-regen` CLI program. It also is used for documentation through `sphinxcontrib-autoprogram`__. :return: a properly configured :class:`~argparse.ArgumentParser` :rtype: :class:`argparse.ArgumentParser` __ https://pythonhosted.org/sphinxcontrib-autoprogram/ """ parser = parser or argparse.ArgumentParser( description='Regen the Geofront master key' ) parser.add_argument('config', metavar='FILE', help='geofront configuration file (Python script)') parser.add_argument('--create-master-key', action='store_true', help='create a new master key if no master key yet') parser.add_argument('-d', '--debug', action='store_true', help='debug mode') parser.add_argument('-v', '--version', action='version', version='%(prog)s ' + VERSION) return parser @typechecked def regenerate(master_key_store: MasterKeyStore, remote_set: RemoteSet, key_type: Type[PKey]=RSAKey, bits: Optional[int]=None, *, create_if_empty: bool, renew_unless_empty: bool) -> None: """Regenerate or create the master key.""" logger = logging.getLogger(__name__ + '.regenerate') try: key = master_key_store.load() except EmptyStoreError: if create_if_empty: logger.warn('no master key; create one...') key = generate_key(key_type, bits) master_key_store.save(key) logger.info('created new master key: %s', get_key_fingerprint(key)) else: raise RegenError('no master key; try --create-master-key option ' 'if you want to create one') else: if renew_unless_empty: renew_master_key(frozenset(remote_set.values()), master_key_store, key_type, bits) class RegenError(Exception): """Error raised by :func:`regenerate()`.""" def get_regen_options(config: Mapping[str, object]) -> Tuple[Type[PKey], Optional[int]]: key_type = config.get('MASTER_KEY_TYPE', RSAKey) if not isinstance(key_type, type): raise RegenOptionError('MASTER_KEY_TYPE configuration must be a type, ' 'not ' + repr(key_type)) elif not issubclass(key_type, PKey): raise RegenOptionError( 'MASTER_KEY_TYPE configuration must be a subclass of ' '{0.__module__}.{0.__qualname__}, but {1.__module__}.' '{1.__qualname__} is not'.format(PKey, key_type) ) bits = config['MASTER_KEY_BITS'] if bits is not None and not isinstance(bits, int): raise RegenOptionError('MASTER_KEY_BITS configuration must be an ' 'integer, not ' + repr(bits)) return RSAKey, bits class RegenOptionError(RegenError): """Error raised by :func:`get_regen_options()`.""" def main(): # pragma: no cover """The main function of :program:`geofront-key-regen` CLI program.""" from .server import app, get_master_key_store, get_remote_set parser = main_parser() args = parser.parse_args() try: app.config.from_pyfile(os.path.abspath(args.config), silent=False) except FileNotFoundError: parser.error('unable to load configuration file: ' + args.config) logger = logging.getLogger('geofront.masterkey') handler = logging.StreamHandler() level = logging.DEBUG if args.debug else logging.INFO handler.setLevel(level) logger.addHandler(handler) logger.setLevel(level) try: regenerate( get_master_key_store(), get_remote_set(), *get_regen_options(app.config), create_if_empty=args.create_master_key, renew_unless_empty=True ) except KeyGenerationError as e: parser.error(str(e)) except RegenError as e: parser.error(str(e))