--- /dev/null
+#!/usr/bin/env python3
+
+# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4
+
+"""
+Copyright © 2020 Frantisek Hrbata <frantisek@hrbata.com>
+This program is free software. It comes without any warranty, to
+the extent permitted by applicable law. You can redistribute it
+and/or modify it under the terms of the Do What The Fuck You Want
+To Public License, Version 2, as published by Sam Hocevar. See
+http://www.wtfpl.net/ for more details.
+"""
+
+import argparse
+import socket
+import gssapi
+import sys
+import os
+import gimrrdataprot
+import select
+import signal
+import tempfile
+import re
+import fcntl
+import logging
+
+parser = argparse.ArgumentParser(description="Server for adding gimrr users public keys using krb authentication.");
+parser.add_argument("--address", metavar="ADDR", default="",
+ help="Bind server to ADDR (default: INADDR_ANY)")
+parser.add_argument("--port", metavar="PORT", default=9876, type=int,
+ help="Listen at specified port (default: 9876)")
+parser.add_argument("--timeout", metavar="SECONDS", default=3, type=int,
+ help="Socket timeout for client connections (default: 3 sec.)")
+parser.add_argument("--principal", metavar="PRINCIPAL",
+ help="Kerberos principal name.")
+parser.add_argument("--keytab", metavar="FILE", default="/etc/krb5.keytab",
+ help="Keytab with key for server principal (default: /etc/krb5.keytab)")
+parser.add_argument("--auth-keys", metavar="FILE", required=True,
+ help="Ssh authorized_keys file where to store public keys.")
+parser.add_argument("--daemon", action="store_true",
+ help="Start server as daemon in background.")
+parser.add_argument("--log", metavar="FILE",
+ help="Server log file.")
+
+args = parser.parse_args()
+os.environ["KRB5_KTNAME"] = args.keytab
+
+logger = logging.getLogger()
+logger.setLevel(logging.DEBUG)
+logfmt = logging.Formatter("%(asctime)s: %(levelname)s - %(message)s")
+
+if args.log:
+ h = logging.FileHandler(args.log)
+ h.setFormatter(logfmt)
+ logger.addHandler(h)
+
+if not args.daemon:
+ h = logging.StreamHandler()
+ h.setFormatter(logfmt)
+ logger.addHandler(h)
+
+server_name = gssapi.Name(args.principal)
+server_cred = gssapi.Credentials(name=server_name, usage='accept')
+
+ss = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
+ss.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
+ss.bind((args.address, args.port))
+ss.listen()
+cs = None
+run = True
+
+def stop(signum, frame):
+ global run
+ run = False
+
+signal.signal(signal.SIGHUP, stop)
+signal.signal(signal.SIGINT, stop)
+signal.signal(signal.SIGTERM, stop)
+
+if args.daemon:
+ pid = os.fork()
+ if pid > 0:
+ sys.exit()
+
+ os.setsid()
+ sys.stdin.close()
+ sys.stdout.close()
+ sys.stderr.close()
+
+tmpdir = tempfile.TemporaryDirectory(dir=os.path.dirname(os.path.realpath(args.auth_keys)))
+auth_keys_fn = "{}/auth_keys".format(tmpdir.name)
+
+logger.info("server started")
+
+while run:
+ try:
+ r, w, e = select.select([ss], [], [], 1)
+ if ss not in r:
+ continue
+
+ # serving users synchronously should be fine for this use case
+ cs, addr = ss.accept()
+ logger.info("connection accepted from {}:{}".format(addr[0], addr[1]))
+
+ ctx = gssapi.SecurityContext(creds=server_cred, usage='accept')
+ while not ctx.complete:
+ data_type, token_in = gimrrdataprot.recv_data(cs)
+ if data_type != gimrrdataprot.DATA_TYPE_AUTH_TOKEN:
+ raise Exception("expecting gssapi auth token")
+
+ token_out = ctx.step(token_in)
+ if not token_out:
+ break
+ gimrrdataprot.send_data(cs, gimrrdataprot.DATA_TYPE_AUTH_TOKEN, token_out)
+
+ if not ctx.complete:
+ raise Exception("no security context")
+
+ user = str(ctx.initiator_name)
+ user = user.split("@")[0]
+ if not user:
+ raise Exception("no user found")
+
+ logger.info("user {} authenticated".format(user))
+
+ data_type, key = gimrrdataprot.recv_data_dec(ctx, cs)
+ if data_type != gimrrdataprot.DATA_TYPE_PUB_KEY:
+ raise Exception("expecting public key")
+
+ # TODO Probably check at least the key format as described in RFC 4253 6.6
+
+ logger.info("received key: {}".format(key.decode().strip()))
+
+ user_re = re.compile('"GIMRR_USER={}"'.format(user))
+
+ with open(args.auth_keys, "a+") as ofd:
+ fcntl.lockf(ofd.fileno(), fcntl.LOCK_EX | fcntl.LOCK_NB)
+ ofd.seek(0)
+ lines = ofd.readlines()
+ with open(os.open(auth_keys_fn, os.O_CREAT | os.O_WRONLY | os.O_TRUNC, 0o600), "w") as nfd:
+ for line in lines:
+ match = user_re.search(line)
+ if not match:
+ nfd.write(line)
+ else:
+ logger.info("removing: {}".format(line.strip()))
+
+ nfd.write('environment="GIMRR_USER={}",restrict,\
+command="/usr/bin/git-shell -c \\"$SSH_ORIGINAL_COMMAND\\"" \
+{}'.format(user, key.decode()))
+
+ os.rename(auth_keys_fn, args.auth_keys)
+ logger.info("new key added for user {}".format(user))
+
+ gimrrdataprot.send_data_enc(ctx, cs, gimrrdataprot.DATA_TYPE_MSG,
+ "public key successfully registered for '{}'".format(user))
+ except Exception as e:
+ logger.error(e)
+ if cs:
+ gimrrdataprot.send_data(cs, gimrrdataprot.DATA_TYPE_ERR, str(e))
+ finally:
+ if cs:
+ cs.close()
+ cs = None
+
+logger.info("server stopped")