Add letsencrypt-tiny wrapper around acme-tiny
[lets-encrypt-tiny.git] / letsencrypt-tiny
1 #!/usr/bin/env python3
2 ## Call with "--help" for documentation.
3
4 import argparse, configparser, itertools, os, os.path, sys, subprocess, datetime
5
6 ## Helper functions
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)
12     return config
13
14 def certfile(name, suff = None):
15     global config
16     return os.path.join(config['dirs']['certs'], name + ".crt" + ('' if suff is None else '+'+suff) )
17
18 def keyfile(name):
19     global config
20     return os.path.join(config['dirs']['keys'], name + ".key")
21
22 def csrfile(name):
23     global config
24     return os.path.join(config['dirs']['csrs'], name + ".csr")
25
26 def make_backup(fname):
27     if os.path.exists(fname):
28         backupname = os.path.basename(fname) + "." + str(datetime.date.today())
29         i = 0
30         while True:
31             backupfile = os.path.join(config['dirs']['backups'], backupname + "." + str(i))
32             if not os.path.exists(backupfile):
33                 os.rename(fname, backupfile)
34                 break
35             elif i >= 100:
36                 print("Somehow it's really hard to find a name for the backup file...")
37             i += 1
38     assert not os.path.exists(fname)
39
40 def trigger_hook(hook):
41     global config
42     exe = config['hooks'][hook]
43     if exe is not None:
44         subprocess.check_call([exe])
45
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:
52         file.write(csr)
53
54 def acme(name, domains):
55     global config
56     print("Obtaining certificate {} for domains {}".format(name, ' '.join(domains)))
57     gencsr(name, 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)
65     # append DH params
66     dhfile = config['DEFAULT']['dh-params']
67     if dhfile is not None:
68         with open(dhfile, 'rb') as file:
69             dh = file.read()
70         make_backup(certfile(name, 'dh'))
71         with open(certfile(name, 'dh'), 'wb') as file:
72             file.write(signed_crt)
73             file.write(dh)
74
75 def getcert(name):
76     global config
77     if not os.path.exists(keyfile(name)):
78         raise Exception("No such key: {}".format(name))
79     domains = config['DEFAULT']['domains'].split()
80     acme(name, domains)
81     trigger_hook('post-cert')
82
83 ## Main
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",
88                         dest="config",
89                         help="The configuration file")
90     parser.add_argument
91     parser.add_argument("action", metavar='ACTION', nargs=1,
92                         help="The action to perform. Possible values: renew, cron")
93     args = parser.parse_args()
94     
95     # read config
96     if not os.path.isfile(args.config):
97         raise Exception("The config file does not exist: "+args.config)
98     global config
99     config = readConfig(args.config)
100     
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)):
106             getcert(staging)
107     else:
108         raise Exception("Unknown action {}".format(args.action))