2 ## Call with "--help" for documentation.
4 import argparse, configparser, itertools, os, os.path, sys, subprocess, datetime
7 def readConfig(fname, defSection = 'DEFAULT'):
8 config = configparser.ConfigParser()
9 with open(fname) as file:
10 stream = itertools.chain(("["+defSection+"]\n",), file)
11 config.read_file(stream)
14 def certfile(name, suff = None):
16 return os.path.join(config['dirs']['certs'], name + ".crt" + ('' if suff is None else '+'+suff) )
20 return os.path.join(config['dirs']['keys'], name + ".key")
24 return os.path.join(config['dirs']['csrs'], name + ".csr")
26 def make_backup(fname):
27 if os.path.exists(fname):
28 backupname = os.path.basename(fname) + "." + str(datetime.date.today())
31 backupfile = os.path.join(config['dirs']['backups'], backupname + "." + str(i))
32 if not os.path.exists(backupfile):
33 os.rename(fname, backupfile)
36 print("Somehow it's really hard to find a name for the backup file...")
38 assert not os.path.exists(fname)
40 def trigger_hook(hook):
42 exe = config['hooks'][hook]
44 subprocess.check_call([exe])
46 ## The interesting work
47 def gencsr(name, domains):
48 # This is done by a shell script
49 exe = os.path.join(os.path.dirname(__file__), 'gencsr')
50 csr = subprocess.check_output([exe, keyfile(name)] + domains)
51 with open(csrfile(name), 'wb') as file:
54 def acme(name, domains):
56 print("Obtaining certificate {} for domains {}".format(name, ' '.join(domains)))
58 # call acme-tiny as a script
59 acme_tiny = os.path.join(config['acme']['acme-tiny'], 'acme_tiny.py')
60 signed_crt = subprocess.check_output([acme_tiny, "--quiet", "--account-key", config['acme']['account-key'], "--csr", csrfile(name), "--acme-dir", config['acme']['challenge-dir']])
61 # save new certificate
62 make_backup(certfile(name))
63 with open(certfile(name), 'wb') as file:
64 file.write(signed_crt)
66 dhfile = config['DEFAULT']['dh-params']
67 if dhfile is not None:
68 with open(dhfile, 'rb') as file:
70 make_backup(certfile(name, 'dh'))
71 with open(certfile(name, 'dh'), 'wb') as file:
72 file.write(signed_crt)
77 if not os.path.exists(keyfile(name)):
78 raise Exception("No such key: {}".format(name))
79 domains = config['DEFAULT']['domains'].split()
81 trigger_hook('post-cert')
84 if __name__ == "__main__":
85 # allow overwriting some values on the command-line
86 parser = argparse.ArgumentParser(description='Generate and (automatically) renew certificates, optionally providing staging for new keys')
87 parser.add_argument("-c", "--config",
89 help="The configuration file")
91 parser.add_argument("action", metavar='ACTION', nargs=1,
92 help="The action to perform. Possible values: renew, cron")
93 args = parser.parse_args()
96 if not os.path.isfile(args.config):
97 raise Exception("The config file does not exist: "+args.config)
99 config = readConfig(args.config)
101 if args.action[0] == 'renew':
102 getcert(config['files']['live'])
103 # We may also have to renew the staging
104 staging = config['files']['staging']
105 if staging is not None and os.path.exists(keyfile(staging)):
108 raise Exception("Unknown action {}".format(args.action))