+#!/usr/bin/env python3
+## Call with "--help" for documentation.
+
+import argparse, configparser, itertools, os, os.path, sys, subprocess, datetime
+
+## Helper functions
+def readConfig(fname, defSection = 'DEFAULT'):
+ config = configparser.ConfigParser()
+ with open(fname) as file:
+ stream = itertools.chain(("["+defSection+"]\n",), file)
+ config.read_file(stream)
+ return config
+
+def certfile(name, suff = None):
+ global config
+ return os.path.join(config['dirs']['certs'], name + ".crt" + ('' if suff is None else '+'+suff) )
+
+def keyfile(name):
+ global config
+ return os.path.join(config['dirs']['keys'], name + ".key")
+
+def csrfile(name):
+ global config
+ return os.path.join(config['dirs']['csrs'], name + ".csr")
+
+def make_backup(fname):
+ if os.path.exists(fname):
+ backupname = os.path.basename(fname) + "." + str(datetime.date.today())
+ i = 0
+ while True:
+ backupfile = os.path.join(config['dirs']['backups'], backupname + "." + str(i))
+ if not os.path.exists(backupfile):
+ os.rename(fname, backupfile)
+ break
+ elif i >= 100:
+ print("Somehow it's really hard to find a name for the backup file...")
+ i += 1
+ assert not os.path.exists(fname)
+
+def trigger_hook(hook):
+ global config
+ exe = config['hooks'][hook]
+ if exe is not None:
+ subprocess.check_call([exe])
+
+## The interesting work
+def gencsr(name, domains):
+ # This is done by a shell script
+ exe = os.path.join(os.path.dirname(__file__), 'gencsr')
+ csr = subprocess.check_output([exe, keyfile(name)] + domains)
+ with open(csrfile(name), 'wb') as file:
+ file.write(csr)
+
+def acme(name, domains):
+ global config
+ print("Obtaining certificate {} for domains {}".format(name, ' '.join(domains)))
+ gencsr(name, domains)
+ # call acme-tiny as a script
+ acme_tiny = os.path.join(config['acme']['acme-tiny'], 'acme_tiny.py')
+ signed_crt = subprocess.check_output([acme_tiny, "--quiet", "--account-key", config['acme']['account-key'], "--csr", csrfile(name), "--acme-dir", config['acme']['challenge-dir']])
+ # save new certificate
+ make_backup(certfile(name))
+ with open(certfile(name), 'wb') as file:
+ file.write(signed_crt)
+ # append DH params
+ dhfile = config['DEFAULT']['dh-params']
+ if dhfile is not None:
+ with open(dhfile, 'rb') as file:
+ dh = file.read()
+ make_backup(certfile(name, 'dh'))
+ with open(certfile(name, 'dh'), 'wb') as file:
+ file.write(signed_crt)
+ file.write(dh)
+
+def getcert(name):
+ global config
+ if not os.path.exists(keyfile(name)):
+ raise Exception("No such key: {}".format(name))
+ domains = config['DEFAULT']['domains'].split()
+ acme(name, domains)
+ trigger_hook('post-cert')
+
+## Main
+if __name__ == "__main__":
+ # allow overwriting some values on the command-line
+ parser = argparse.ArgumentParser(description='Generate and (automatically) renew certificates, optionally providing staging for new keys')
+ parser.add_argument("-c", "--config",
+ dest="config",
+ help="The configuration file")
+ parser.add_argument
+ parser.add_argument("action", metavar='ACTION', nargs=1,
+ help="The action to perform. Possible values: renew, cron")
+ args = parser.parse_args()
+
+ # read config
+ if not os.path.isfile(args.config):
+ raise Exception("The config file does not exist: "+args.config)
+ global config
+ config = readConfig(args.config)
+
+ if args.action[0] == 'renew':
+ getcert(config['files']['live'])
+ # We may also have to renew the staging
+ staging = config['files']['staging']
+ if staging is not None and os.path.exists(keyfile(staging)):
+ getcert(staging)
+ else:
+ raise Exception("Unknown action {}".format(args.action))