#!/usr/bin/python3 # SPDX-License-Identifier: AGPL-3.0-or-later """ Module with utilities to generate a auth_pubtkt ticket and sign it with the FreedomBox server's private key. """ import argparse import base64 import datetime import os from OpenSSL import crypto KEYS_DIRECTORY = '/etc/apache2/auth-pubtkt-keys' def parse_arguments(): """ Return parsed command line arguments as dictionary. """ parser = argparse.ArgumentParser() subparsers = parser.add_subparsers(dest='subcommand', help='Sub command') subparsers.add_parser( 'create-key-pair', help='create a key pair for the apache server ' 'to sign auth_pubtkt tickets') gen_tkt = subparsers.add_parser('generate-ticket', help='generate auth_pubtkt ticket') gen_tkt.add_argument('--uid', help='username of the user') gen_tkt.add_argument('--private-key-file', help='path of the private key file of the server') gen_tkt.add_argument('--tokens', help='tokens, usually containing the user groups') subparsers.required = True return parser.parse_args() def subcommand_create_key_pair(_): """Create public/private key pair for signing the auth_pubtkt tickets. """ private_key_file = os.path.join(KEYS_DIRECTORY, 'privkey.pem') public_key_file = os.path.join(KEYS_DIRECTORY, 'pubkey.pem') os.path.exists(KEYS_DIRECTORY) or os.mkdir(KEYS_DIRECTORY) if not all([ os.path.exists(key_file) for key_file in [public_key_file, private_key_file] ]): pkey = crypto.PKey() pkey.generate_key(crypto.TYPE_RSA, 4096) with open(private_key_file, 'w') as priv_key_file: priv_key = crypto.dump_privatekey(crypto.FILETYPE_PEM, pkey).decode() priv_key_file.write(priv_key) with open(public_key_file, 'w') as pub_key_file: pub_key = crypto.dump_publickey(crypto.FILETYPE_PEM, pkey).decode() pub_key_file.write(pub_key) for fil in [public_key_file, private_key_file]: os.chmod(fil, 0o440) def create_ticket(pkey, uid, validuntil, ip=None, tokens=None, udata=None, graceperiod=None, extra_fields=None): """Create and return a signed mod_auth_pubtkt ticket.""" fields = [ f'uid={uid}', f'validuntil={int(validuntil)}', ip and f'cip={ip}', tokens and f'tokens={tokens}', graceperiod and f'graceperiod={int(graceperiod)}', udata and f'udata={udata}', extra_fields and ';'.join(['{}={}'.format(k, v) for k, v in extra_fields]), ] data = ';'.join(filter(None, fields)) signature = 'sig={}'.format(sign(pkey, data)) return ';'.join([data, signature]) def sign(pkey, data): """Calculates and returns ticket's signature.""" sig = crypto.sign(pkey, data.encode(), 'sha512') return base64.b64encode(sig).decode() def subcommand_generate_ticket(arguments): """Generate a mod_auth_pubtkt ticket using login credentials.""" uid = arguments.uid private_key_file = arguments.private_key_file tokens = arguments.tokens with open(private_key_file, 'r') as fil: pkey = crypto.load_privatekey(crypto.FILETYPE_PEM, fil.read().encode()) valid_until = minutes_from_now(12 * 60) grace_period = minutes_from_now(11 * 60) print( create_ticket(pkey, uid, valid_until, tokens=tokens, graceperiod=grace_period)) def minutes_from_now(minutes): """Return a timestamp at the given number of minutes from now.""" return seconds_from_now(minutes * 60) def seconds_from_now(seconds): """Return a timestamp at the given number of seconds from now.""" return (datetime.datetime.now() + datetime.timedelta(0, seconds)).timestamp() def main(): """Parse arguments and perform all duties.""" arguments = parse_arguments() subcommand = arguments.subcommand.replace('-', '_') subcommand_method = globals()['subcommand_' + subcommand] subcommand_method(arguments) if __name__ == '__main__': main()