#!/usr/bin/env python

from __future__ import print_function

"""A simple Signing Server

We listen on the configured port, and use a number of children to service
each connection.

configuration
=============

Below are the known configuration items with their default values:

    Signer=RSASigner
    	name of class to do signing 

    SigningKey=
    	key suitable for Signer

    min_size=0
    	reject requests which supply less data than this.
    	There isn't much point using sha256 in a signature if the
        client sends a sha1 or md5 hash.

    PEMTag=SIGNATURE
    	tag to name the signaure in the PEM header

    SigExt=.sig
    	we tell the client which extention we use.

    SigHeader=
    	any random string will be prepended to signatur
    	the DN of the signing cert is a common value

    syslogFacility=
    	syslog facility

    syslogOptions=
    	syslog options

    ListenAddress=0.0.0.0

    ListenPort=0

    allow_nets=
    	networks and addresses allowed to connect
        If this is set, then the default behavior is to reject
        connections.
        
    deny_nets=
    	networks and addresses not allowed to connect

    allow_ctl=
    	list of networks and addresses allowed to send control messages
        if allow_nets is set, allow_ctl will be added to it if needed.

    deny_ctl
        list of networks and addresses not allowed to send control messages

    timeout=5
    	when a connection is accepted, wait at most timeout seconds for input
        else drop the connection.

Signing request
===============

We expect the client to send us a suitable hexdigest from a hash function
compatible with the one we use for signing, a traling newline is assumed.
The goal being to make it easy to implement the client as a shell script.

By default we respond with a PEM encoded signature.
We also output a control block to tell the client the expected
signature to use.
Eg.

#set: sig_ext=.sig
-----BEGIN RSA SIGNATURE-----
053CBh8L0lgdrydhVCELOkfZwfsHyQNmHelS+scvu/1jddyGM3JdWR3sfSI47267
EuUQw8H/ihW2O3iGPppIx21zoegul12bocP/MIpZooiXiya93iyUoYZrVDI9QntN
9cpQbF7FbdWg2TR7u71vO8OXWR/9yeuGpDuoYmqjJf5IxIDiE8Vs2WVpH5dkopMe
5XdMNFKEsmBpnyLykBAAKTlNrUYjdKMhoiKkLIalmIEXLNV85Ocb+PaxAWdQWnGJ
OJAZbIW9Pv4MmBaaY7X2QESLldj4ee9JHVOLls5cjhT2jriaSYPL5EeFmNezU4z0
sHo64MikEN1VAs5vTETOrw==
-----END RSA SIGNATURE-----

The #set: cannot be misstaken for PEM encoded data.

If there is any error it will begin with ERROR:

Control messages
================

The client can send control requests too:

    certs: we will respond with the certificate chain associated with
    	   the signing key (assuming it has been configured).

    crls:   we will respond with contents of configured crl blob.

    signctl: shutdown
    	   perform an orderly shutdown

"""

"""
RCSid:
	$Id: SignServerPool.py,v 1.41 2025/04/19 18:27:55 sjg Exp $

	@(#) Copyright (c) 2012 Simon J. Gerraty

	This file is provided in the hope that it will
	be of use.  There is absolutely NO WARRANTY.
	Permission to copy, redistribute or otherwise
	use this file is hereby granted provided that 
	the above copyright notice and this notice are
	left intact. 
      
	Please send copies of changes and bug-fixes to:
	sjg@crufty.net

"""

import os
import signal
import sys
import errno
from syslog import *
import conf as cf
from p3compat import *
import PoolServer
import IPacl
            
class SignServerPool(PoolServer.PoolServer):

    def server_setup(self):
        """setup data that our handler needs"""
        self.debug = int(self.conf.get('debug', 0))
        self.timeout = int(self.conf.get('timeout', 5))
        kfile = self.conf.get('SigningKey')
        clsname = self.conf.get('Signer', 'OpenSSLSigner')
        sm = __import__(clsname)
        sc = getattr(sm, clsname)
        self.signer = sc(kfile, self.conf)
        self.min_size = int(self.conf.get('min_size', 0))
        self.pem_tag = self.conf.get('PEMTag', 'SIGNATURE')
        self.recv_size = int(conf.get('recv_size', 2048))
        self.sign_log_keys = self.conf.get('SignLogKeys', 'user,path').split(',')
        self.sign_log_key_clue = self.sign_log_keys[0] + '='
        self.sign_log_fmt = 'received {hash:.64} from: {client}'
        for k in self.sign_log_keys:
            self.sign_log_fmt += ' {}: {}{}{}'.format(k,'{',k,'}')
        sig_ext = self.conf.get('SigExt', '.sig')
        hdr = self.conf.get('SigHeader')
        set_knobs = self.conf.get('SetKnobs', '')
        he = ['#set: sig_ext={} {}'.format(sig_ext, set_knobs)]
        if hdr:
            he.append(hdr)
        self.sig_header = b('\n'.join(he)+'\n')
        certs = self.conf.get('Certs')
        if certs:
            self.certs = open(certs).read()
        else:
            self.certs = 'ERROR: no cert chain'
        crls = self.conf.get('CRLs')
        if crls:
            self.crl_data = open(crls).read()
        else:
            self.crl_data = 'ERROR: no CRL data'
        ta = self.conf.get('TrustAnchor')
        if ta:
            self.ta_data = open(ta).read()
        else:
            self.ta_data = 'ERROR: no trust anchor - try certs'
        # implement allow/deny acls
        allow_nets = self.conf.get('allow_nets')
        deny_nets = self.conf.get('deny_nets')
        if allow_nets or deny_nets:
            self.ip_acl = IPacl.IPacl()
            if allow_nets:
                self.ip_acl.allow(allow_nets.split())
            if deny_nets:
                self.ip_acl.deny(deny_nets.split())
        else:
            self.ip_acl = None

        # and ctl requests can be from a subset of the above
        allow_ctl = self.conf.get('allow_ctl')
        deny_ctl = self.conf.get('deny_ctl')
        if allow_ctl or deny_ctl:
            self.ctl_acl = IPacl.IPacl()
            if allow_ctl:
                self.ctl_acl.allow(allow_ctl.split())
                if allow_nets:
                    self.ip_acl.allow(allow_ctl.split())
            if deny_ctl:
                self.ctl_acl.deny(deny_ctl.split())
        else:
            self.ctl_acl = None

        syslog(LOG_DEBUG, 'Signer: {0}, PEMtag: {1}, SigExt: {2}'.format(self.signer, self.pem_tag, sig_ext))

    def verify_request(self, request, client_address):
        if self.ip_acl:
            if not self.ip_acl.check_ip(client_address[0]):
                return False
        return True

    def process_request(self, request, client_address):

        self.request = request
        self.client_address = client_address
        self.signer.begin(request, client_address)
        self.process_body()
        self.signer.end(request, client_address)

    def process_body(self):
        while True:
            try:
                signal.alarm(self.timeout)
                msg = self.request.recv(self.recv_size)
                signal.alarm(0)
            except NameError:
                raise
            except Exception as e:
                if e.errno in [errno.EINTR,errno.EIO]:
                    return
                if e.errno == errno.ETIMEDOUT:
                    syslog(LOG_NOTICE, 'timeout waiting for {}'.format(self.client_address))
                    return
                if self.debug:
                    print('caught: {} {}'.format(e.errno, e.strerror))
                raise
            if not msg:
                return
            if msg.startswith(b'signctl:'):
                self.control(s(msg))
                return
            if msg.startswith(b'certs:'):
                self.cert_chain()
                return
            if msg.startswith(b'crls:'):
                self.crl_req()
                return
            if msg.startswith(b'ta:'):
                self.ta_req()
                return
            self.sign_msg(msg)

    def sign_msg(self, msg):
        """Sign a message from client"""

        # everything below assumes msg is string
        msg = s(msg)
        d = {}
        for k in self.sign_log_keys:
            d[k] = 'unknown'
        d['client'] = self.client_address
        if msg.find(self.sign_log_key_clue) >= 0:
            for kv in msg.split():
                try:
                    k,v = kv.split('=', 1)
                    if k in self.sign_log_keys:
                        d[k] = v
                    if k == 'hash':
                        # the newline is needed for backwards compatability
                        d[k] = v + '\n'
                except:
                    pass
        else:
            d['hash'] = msg
            
        syslog(LOG_INFO, self.sign_log_fmt.format(**d))
        if self.min_size > 0 and len(d['hash']) < self.min_size:
            response = 'ERROR: Not enough data'
        else:
            try:
                sig = self.signer.sign(d['hash'])
                response = self.sig_header + \
                           self.signer.encode_sig(sig, self.pem_tag)
            except Exception as e:
                response = 'ERROR: {}'.format(str(e))
                syslog(LOG_NOTICE, s(response))
        return self.request.send(b(response))

    def cert_chain(self):
        syslog(LOG_INFO, 'certs request from: {}'.format(self.client_address))
        return self.request.send(b(self.certs))

    def crl_req(self):
        syslog(LOG_INFO, 'crl request from: {}'.format(self.client_address))
        return self.request.send(b(self.crl_data))

    def ta_req(self):
        syslog(LOG_INFO, 'ta request from: {}'.format(self.client_address))
        return self.request.send(b(self.ta_data))

    def control(self, msg):
        """process a control message"""
        if self.ctl_acl:
            if not self.ctl_acl.check_ip(self.client_address[0]):
                syslog(LOG_NOTICE, 'rejected {0} from: {1}'.format(msg,self.client_address))
                return
                
        w = msg.strip().split()
        if w[0] == 'signctl:':
            if w[1] == 'shutdown':
                self.request.send(b('{} ACK\n'.format(w[1])))
                # this logs the request
                self.request_shutdown(self.request, self.client_address)
            syslog(LOG_NOTICE, '{0} request from: {1}'.format(w[1], self.client_address))

if __name__ == '__main__':
    """Unit test, simulate sending hash via protobuf to server for signing"""
    import getopt, os.path
    from hashlib import sha1

    conf = {}
    debug = 0
    
    opts,args = getopt.getopt(sys.argv[1:], 'dL:O:t:k:c:a:p:n:')
    for o,a in opts:
        if o == '-c':
            conf = cf.loadConfig(a, conf)
        elif o == '-L':
            conf['syslogFacility'] = a
        elif o == '-O':
            conf['syslogOptions'] = a
        elif o == '-a':
            conf['ListenAddress'] = a
        elif o == '-p':
            conf['ListenPort'] = a
        elif o == '-t':
            conf['Signer'] = a + 'Signer'
        elif o == '-k':
            conf['SigningKey'] = a
        elif o == '-d':
            debug += 1
            conf['debug'] = debug
        elif o == '-n':
            conf['children'] = a


    addr = conf.get('ListenAddress', '0.0.0.0')
    port = int(conf.get('ListenPort', '0'))
    kfile = conf.get('SigningKey')
    logfac = conf.get('syslogFacility')
    if logfac:
        m = sys.modules['syslog']
        w = logfac.upper().split('.')
        fac = getattr(m, 'LOG_'+w[0])
        if len(w) > 1:
            pri = getattr(m, 'LOG_'+w[1])
        elif debug:
            pri = LOG_DEBUG
        else:
            pri = LOG_INFO

        logopts = LOG_PID
        if debug:
            logopts |= LOG_PERROR
        opts = conf.get('syslogOptions')
        if opts:
            for o in opts.split('|'):
                logopts += getattr(m, o)

        ident = conf.get('syslogIdent')
        try:
            if not ident:
                ident = os.path.splitext(os.path.basename(kfile))[0]
        except:
            pass
        if not ident:
            ident = 'SignServer'
        openlog(ident, logopts, fac)
        setlogmask(LOG_UPTO(pri))

    nchild = int(conf.get('children', 8))
    server = SignServerPool((addr, port), nchild, conf)
    server.server_loop()
