From: Ralf Jung Date: Sun, 13 Dec 2015 12:25:20 +0000 (+0100) Subject: Add letsencrypt-tiny wrapper around acme-tiny X-Git-Url: https://git.ralfj.de/lets-encrypt-tiny.git/commitdiff_plain/b2e264ae28b45fe07844a7d9d19f8e7f81cf40cf?ds=sidebyside Add letsencrypt-tiny wrapper around acme-tiny --- diff --git a/letsencrypt-tiny b/letsencrypt-tiny new file mode 100755 index 0000000..0d8c985 --- /dev/null +++ b/letsencrypt-tiny @@ -0,0 +1,108 @@ +#!/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)) diff --git a/letsencrypt-tiny.conf.sample b/letsencrypt-tiny.conf.sample new file mode 100644 index 0000000..d2535e5 --- /dev/null +++ b/letsencrypt-tiny.conf.sample @@ -0,0 +1,36 @@ +# A sample config file for letsencrypt-tiny + +# List of domains for the cert to apply to. +domains = + example.org + example.com + +# File containing the DH parameters, as generated by openssl (optional) +dh-params = /etc/ssl/dh2048.pem + +[hooks] +# Called after a new certificate has been obtained. +# Example usage: Reloading services. +post-cert = /home/user/letsencrypt/cert-hook +# Called after a new certificate has been obtained, *if* there also were changes in the private keys +# Example usage: Updating TLSA records (with the selector being SubjectPublicKeyInfo) in the zone +post-key = /home/user/letsencrypt/key-hook + +# Parameters for acme-tiny +[acme] +acme-tiny = /home/user/letsencrypt/acme-tiny/ +account-key = /etc/ssl/private/letsencrypt/account.key +challenge-dir = /srv/acme-challenge/ + +# Where to store all the things. +[dirs] +certs = /etc/ssl/mycerts/letsencrypt +keys = /etc/ssl/private/letsencrypt +csrs = /etc/ssl/private/letsencrypt +backups = /etc/ssl/old/letsencrypt + +[files] +# Base name of the live key and certificate +live = live +# Base name of the staging key and certificate (optional) +staging = staging