From 51cfaa176a021af7f611f3ffe024bafc99b696d0 Mon Sep 17 00:00:00 2001 From: Christoph Egger Date: Sat, 9 Dec 2017 20:57:31 +0100 Subject: [PATCH] Change everything --- .gitignore | 1 + bin/newcert | 145 ++++-------------------------- bin/update | 65 ++++++++++++++ config/inventory.yaml | 113 ----------------------- sicceggetools/acme/authorize.py | 85 ++++++++++++++++++ sicceggetools/acme/certificate.py | 67 ++++++++++++++ sicceggetools/acme/client.py | 67 ++++++++++++++ sicceggetools/acme/constants.py | 3 +- sicceggetools/acme/settings.py | 29 ++++++ sicceggetools/inventory.py | 16 ++++ 10 files changed, 347 insertions(+), 244 deletions(-) create mode 100644 .gitignore create mode 100755 bin/update delete mode 100644 config/inventory.yaml create mode 100644 sicceggetools/acme/authorize.py create mode 100644 sicceggetools/acme/certificate.py create mode 100644 sicceggetools/acme/client.py create mode 100644 sicceggetools/acme/settings.py create mode 100644 sicceggetools/inventory.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b25c15b --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +*~ diff --git a/bin/newcert b/bin/newcert index 5b5f9a0..1188cba 100755 --- a/bin/newcert +++ b/bin/newcert @@ -8,148 +8,35 @@ from socket import getfqdn import argparse import logging import os.path -import yaml import time +import sys -from acme import client from acme import jose -from acme import messages -from acme import challenges - -from cryptography.hazmat.backends import default_backend -from cryptography.hazmat.primitives.asymmetric import rsa -from cryptography.hazmat.primitives import serialization -from cryptography.hazmat.primitives import hashes -from cryptography import x509 -from cryptography.x509.oid import NameOID import OpenSSL +import yaml -import pexpect - -from sicceggetools.acme import constants - - -def get_client(): - logging.info("Loading account key") - with open("data/account.key.pem", "rb") as keyfd: - private_key = serialization.load_pem_private_key( - keyfd.read(), - password=None, - backend=default_backend() - ) - - logging.info("Loading account registration") - with open("data/registration.json", "rb") as regfd: - registration = messages.RegistrationResource.json_loads(regfd.read()) - - account_key = jose.JWKRSA(key=private_key) - acme_client = client.Client(constants.DIRECTORY_URL, account_key) - - return registration, acme_client, account_key - - -def authorize(sans): - registration, acme_client, account_key = get_client() - authorizations = [] - for san in sans: - authzr = acme_client.request_challenges( - identifier=messages.Identifier(typ=messages.IDENTIFIER_FQDN, value=san), - new_authzr_uri=registration.new_authzr_uri) - authorizations.append(authzr) - for challenge in authzr.body.challenges: - if isinstance(challenge.chall, challenges.DNS01): - print(san) - print(challenge.validation(account_key)) - print(challenge.key_authorization(account_key)) - ssh = pexpect.spawn("ssh _tls@ns1.siccegge.de acme") - ssh.expect("Hostname:") - ssh.sendline(san) - ssh.expect("Value:") - ssh.sendline(challenge.validation(account_key)) - ssh.expect("OK") - - break - else: - print("fallthrough") - - time.sleep(5) - for authorization in authorizations: - for challenge in authorization.body.challenges: - if isinstance(challenge.chall, challenges.DNS01): - response = challenges.DNS01Response(key_authorization=challenge.key_authorization(account_key)) - acme_client.answer_challenge(challenge, response) - - while(True): - print("sleeping") - time.sleep(5) - new_authorizations = [] - for authorization in authorizations: - new_auth, response = acme_client.poll(authorization) - new_authorizations.append(new_auth) - if new_auth.body.status != messages.Status("valid"): - break - else: - return new_authorizations - - -def get_certificate(servicetype, cname, sans): - registration, acme_client, account_key = get_client() - authorizations = authorize(sans) - - with open(os.path.join("certs", servicetype, cname, "key.pem"), "rb") as keyfd: - private_key = serialization.load_pem_private_key( - keyfd.read(), - password=None, - backend=default_backend()) - - builder = x509.CertificateSigningRequestBuilder() - builder = builder.subject_name(x509.Name([ - x509.NameAttribute(NameOID.COMMON_NAME, cname.decode()), - ])) - builder = builder.add_extension( - x509.SubjectAlternativeName([x509.DNSName(x.decode()) for x in sans]), - critical=False) - - request = builder.sign(private_key, hashes.SHA512(), default_backend()) - orequest = OpenSSL.crypto.load_certificate_request( - OpenSSL.crypto.FILETYPE_PEM, - request.public_bytes(serialization.Encoding.PEM)) - - jrequest = jose.util.ComparableX509(orequest) - cert = acme_client.request_issuance(jrequest, authorizations) - certs = acme_client.fetch_chain(cert) - - with open(os.path.join("certs", servicetype, cname, "cert.pem"), "wb") as certfd: - certfd.write(cert.body._dump(OpenSSL.crypto.FILETYPE_PEM)) - for cert in certs: - certfd.write(cert._dump(OpenSSL.crypto.FILETYPE_PEM)) +sys.path.append(os.path.expanduser("~")) +from sicceggetools.acme.client import Client +from sicceggetools.acme.authorize import authorize +from sicceggetools.inventory import Inventory +from sicceggetools.acme.settings import Settings - print(cname) - print(sans) - print(cert) - def main(): + logging.getLogger().setLevel(logging.INFO) + parser = argparse.ArgumentParser() - parser.add_argument('--servicetype', '-s', type=str) parser.add_argument('certificate', type=str) args = parser.parse_args() - - with open("config/inventory.yaml") as invfd: - inventory = yaml.load(invfd.read()) - certificate_list = inventory[getfqdn()][args.servicetype] - if type(certificate_list) is list: - if args.certificate in certificate_list: - get_certificate(args.servicetype, args.certificate, [args.certificate]) - elif type(certificate_list) is dict: - if args.certificate in certificate_list.keys(): - get_certificate(args.servicetype, args.certificate, certificate_list[args.certificate]) - else: - print("unexpected type: %s", type(certificate_list)) - - + inventory = Inventory("config/inventory.yaml") + settings = Settings("config/settings.yaml") + + client = Client(inventory, settings); + client.get_certificate(args.certificate, args.servicetype) + + if __name__ == '__main__': main() diff --git a/bin/update b/bin/update new file mode 100755 index 0000000..ae6f208 --- /dev/null +++ b/bin/update @@ -0,0 +1,65 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# (C) Christoph Egger + +from __future__ import print_function + +import glob +import datetime +import logging +import sys +import os + +from IPython import embed + +from cryptography import x509 +from cryptography.hazmat.backends import default_backend + +sys.path.append(os.path.expanduser("~")) +from sicceggetools.inventory import Inventory +from sicceggetools.acme.settings import Settings +from sicceggetools.acme.constants import SERVICETYPES +from sicceggetools.acme.client import Client + +def find_old_certificates(): + now = datetime.datetime.now() + result = dict() + for stype in SERVICETYPES: + result[stype] = [] + for cert in glob.glob("certs/%s/*/cert.pem" % stype): + with open(cert) as pem: + certdata = x509.load_pem_x509_certificate(pem.read(), default_backend()) + + if (certdata.not_valid_after - now) < datetime.timedelta(days=30): + for attribute in certdata.subject: + if attribute.oid == x509.OID_COMMON_NAME: + result[stype].append((cert, attribute.value)) + break + + return result + + + +def main(): + logging.getLogger().setLevel(logging.INFO) + + # parser = argparse.ArgumentParser() + # parser.add_argument('--servicetype', '-s', type=str) + # parser.add_argument('certificate', type=str) + # args = parser.parse_args() + + inventory = Inventory("config/inventory.yaml") + settings = Settings("config/settings.yaml") + + oldcerts = find_old_certificates() + + for stype in SERVICETYPES: + for path, name in oldcerts[stype]: + print(path, name) + + client = Client(inventory, settings); + client.get_certificate(name, stype) + + +if __name__ == '__main__': + main() diff --git a/config/inventory.yaml b/config/inventory.yaml deleted file mode 100644 index da56f6b..0000000 --- a/config/inventory.yaml +++ /dev/null @@ -1,113 +0,0 @@ -gandi.siccegge.de: - www: - siccegge.de: - - siccegge.de - - static.siccegge.de - - www.siccegge.de - pim.siccegge.de: - - kalender.egger.im - - pim.siccegge.de - - radicale.siccegge.de - frida.xyz: - - doc.frida.xyz - - frida.xyz - - www.frida.xyz - avatars.siccegge.de: - - avatars.siccegge.de - fsinf.fau.fail: - - fsinf.fau.fail - git.siccegge.de: - - git.siccegge.de - katarakt.faui2k9.de: - - katarakt.faui2k9.de - pdfgrep.org: - - pdfgrep.org - - www.pdfgrep.org - planet.faui2k9.de: - - planet.faui2k9.de - privat.egger.im: - - privat.egger.im - taz.siccegge.de: - - taz.siccegge.de - weblog.siccegge.de: - - weblog.siccegge.de - fau.fail: - - fau.fail - - www.fau.fail - xmpp: - conference.faui2k9.de: - - faui2k9.de - - conference.faui2k9.de - - egger.im - - conference.egger.im - smtp: - mx4.siccegge.de: - - mx4.siccegge.de - - gandi.siccegge.de - -chadwick.siccegge.de: - smtp: - mx2.siccegge.de: - - mx2.siccegge.de - - chadwick.siccegge.de - www: - lists.faui2k9.de: - - lists.christoph-egger.org - - lists.faui2k9.de - - lists.frida.xyz - - lists.pdfgrep.org - tt-rss.faui2k9.de: - - tt-rss.faui2k9.de - -1und1.siccegge.de: - smtp: - mx3.siccegge.de: - - mx3.siccegge.de - - 1und1.siccegge.de - imap: - mx3.siccegge.de: - - mx3.siccegge.de - www: - 1und1.siccegge.de: - - 1und1.siccegge.de - jbn-bobingen.de: - - backend.jbn-bobingen.de - - jbn-bobingen.de - - www.jbn-bobingen.de - christoph-egger.org: - - christoph-egger.org - - www.christoph-egger.org - poll.faui2k9.de: - - poll.faui2k9.de - stats.siccegge.de: - - stats.coders-nemesis.eu - - stats.siccegge.de - meetings.christoph-egger.org: - - meetings.christoph-egger.org - -oteiza.siccegge.de: - smtp: - mx1.siccegge.de: - - mx1.siccegge.de - - oteiza.siccegge.de - imap: - imap.siccegge.de: - - imap.siccegge.de - - oteiza.siccegge.de - -bistolfi.siccegge.de: - www: - wot.siccegge.de: - - wot.christoph-egger.org - - wot.siccegge.de - annex.siccegge.de: - - annex.siccegge.de - projects.faui2k9.de: - - projects.faui2k9.de - - userdata.projects.faui2k9.de - -phabricator.siccegge.de: - www: - projects.faui2k9.de: - - projects.faui2k9.de - - userdata.projects.faui2k9.de diff --git a/sicceggetools/acme/authorize.py b/sicceggetools/acme/authorize.py new file mode 100644 index 0000000..3fe4bd2 --- /dev/null +++ b/sicceggetools/acme/authorize.py @@ -0,0 +1,85 @@ +#!/usr/bin/python + +from functools import partial +import logging +import os.path +import time + +import pexpect + +from acme import messages +from acme import challenges + + +def _authorize_dns01(san, validation): + logging.info("Using DNS-01 for %s", san) + ssh = pexpect.spawn("ssh _tls@ns1.siccegge.de acme") + ssh.expect("Hostname:") + ssh.sendline(san) + ssh.expect("Value:") + ssh.sendline(validation) + ssh.expect("OK") + + +def _authorize_http01(san, key_auth): + logging.info("Using HTTP-01 for %s", san) + with open(os.path.join('/srv/tls/http-01/', key_auth.split('.')[0]), 'w') as fd: + fd.write(key_auth) + + +def _authorize_challenge(san, thechallenges, client, settings=None): + _, acme_client, account_key = client + responsefun = None + + for challenge in thechallenges: + if settings.use_method("HTTP01", san, settings) and isinstance(challenge.chall, challenges.HTTP01): + def _response(challenge): + response = challenges.HTTP01Response(key_authorization=challenge.key_authorization(account_key)) + acme_client.answer_challenge(challenge, response) + + _authorize_http01(san, challenge.key_authorization(account_key)) + responsefun = partial(_response, challenge) + + elif settings.use_method("DNS01", san, settings) and isinstance(challenge.chall, challenges.DNS01): + def _response(challenge): + response = challenges.DNS01Response(key_authorization=challenge.key_authorization(account_key)) + acme_client.answer_challenge(challenge, response) + + _authorize_dns01(san, challenge.validation(account_key)) + responsefun = partial(_response, challenge) + + return responsefun + + +def authorize(sans, client, settings=None): + registration, acme_client, _ = client + authorizations = [] + responsefuns = [] + + for san in sans: + authzr = acme_client.request_challenges( + identifier=messages.Identifier(typ=messages.IDENTIFIER_FQDN, value=san), + new_authzr_uri=registration.new_authzr_uri) + authorizations.append(authzr) + + result = _authorize_challenge(san, authzr.body.challenges, client, settings) + if result is None: + logging.warn("fallthrough") + else: + responsefuns.append(result) + + time.sleep(5) + for respfun in responsefuns: + respfun() + + while True: + logging.info("sleeping") + time.sleep(5) + new_authorizations = [] + for authorization in authorizations: + new_auth, _ = acme_client.poll(authorization) + new_authorizations.append(new_auth) + if new_auth.body.status != messages.Status("valid"): + break + else: + return new_authorizations diff --git a/sicceggetools/acme/certificate.py b/sicceggetools/acme/certificate.py new file mode 100644 index 0000000..4ff1496 --- /dev/null +++ b/sicceggetools/acme/certificate.py @@ -0,0 +1,67 @@ +#!/usr/bin/python + +import logging +import os +import os.path + +from cryptography import x509 +from cryptography.hazmat.backends import default_backend +from cryptography.hazmat.primitives import hashes +from cryptography.hazmat.primitives import serialization +from cryptography.x509.oid import NameOID + +import OpenSSL + +class Certificate: + def __init__(self, servicetype, name, sans): + self._name = name + self._sans = sans + self._servicetype = servicetype + self._basename = os.path.join("certs", servicetype, name) + if os.path.exists(os.path.join(self._basename, "key.pem")): + self._from_private_key() + elif os.path.exists(os.path.join(self._basename, "csr.pem")): + self._from_csr() + else: + self._from_scratch() + + + def _from_private_key(self): + with open(os.path.join(self._basename, "key.pem"), "rb") as keyfd: + private_key = serialization.load_pem_private_key( + keyfd.read(), + password=None, + backend=default_backend()) + + builder = x509.CertificateSigningRequestBuilder() + builder = builder.subject_name(x509.Name([ + x509.NameAttribute(NameOID.COMMON_NAME, self._name.decode()), + ])) + builder = builder.add_extension( + x509.SubjectAlternativeName([x509.DNSName(x.decode()) for x in self._sans]), + critical=False) + + request = builder.sign(private_key, hashes.SHA512(), default_backend()) + self._requeststring = request.public_bytes(serialization.Encoding.PEM) + + + def _from_csr(self): + if os.path.exists(os.path.join(self._basename, "csr.pem")): + with open(os.path.join(self._basename, "csr.pem"), "rb") as csrfd: + self._requeststring = csrfd.read() + + + def _from_scratch(self): + raise NotImplementedError("Key generation is currently not implemented") + + + def asString(self): + return self._requeststring + + + def save(self, certificate, chain): + with open(os.path.join(self._basename, "cert.pem"), "wb") as certfd: + certfd.write(certificate.body._dump(OpenSSL.crypto.FILETYPE_PEM)) + for cert in chain: + certfd.write(cert._dump(OpenSSL.crypto.FILETYPE_PEM)) + diff --git a/sicceggetools/acme/client.py b/sicceggetools/acme/client.py new file mode 100644 index 0000000..12b15fe --- /dev/null +++ b/sicceggetools/acme/client.py @@ -0,0 +1,67 @@ +#!/usr/bin/python + +import logging +from socket import getfqdn + + +from cryptography.hazmat.backends import default_backend +from cryptography.hazmat.primitives import serialization + +from acme import client +from acme import jose +from acme import messages + +import OpenSSL + +from . import constants +from .authorize import authorize +from .certificate import Certificate + + + +class Client(object): + def __init__(self, inventory, settings): + self._inventory = inventory + self._settings = settings + self._client = None + + + def _get_client(self): + if self._client is None: + logging.info("Loading account key") + with open("data/account.key.pem", "rb") as keyfd: + private_key = serialization.load_pem_private_key( + keyfd.read(), + password=None, + backend=default_backend() + ) + + logging.info("Loading account registration") + with open("data/registration.json", "rb") as regfd: + registration = messages.RegistrationResource.json_loads(regfd.read()) + + account_key = jose.JWKRSA(key=private_key) + acme_client = client.Client(constants.DIRECTORY_URL, account_key) + self._client = registration, acme_client, account_key + + return self._client + + + def get_certificate(self, cname, servicetype): + sans = self._inventory.get_sans(getfqdn(), servicetype, cname) + + _, acme_client, _ = self._get_client() + authorizations = authorize(sans, self._get_client(), self._settings) + certificate = Certificate(servicetype, cname, sans) + + orequest = OpenSSL.crypto.load_certificate_request( + OpenSSL.crypto.FILETYPE_PEM, certificate.asString()) + + jrequest = jose.util.ComparableX509(orequest) + cert = acme_client.request_issuance(jrequest, authorizations) + chain = acme_client.fetch_chain(cert) + + certificate.save(cert, chain) + + logging.info("CName: %s", cname) + logging.info("SANs: %s", sans) diff --git a/sicceggetools/acme/constants.py b/sicceggetools/acme/constants.py index b8a5c70..076c906 100644 --- a/sicceggetools/acme/constants.py +++ b/sicceggetools/acme/constants.py @@ -1,6 +1,5 @@ DIRECTORY_URL = 'https://acme-v01.api.letsencrypt.org/directory' - KEY_SIZE = 4096 - CONTACT = ('mailto:certmaster@siccegge.de',) +SERVICETYPES = ('xmpp', 'www', 'smtp', 'imap') diff --git a/sicceggetools/acme/settings.py b/sicceggetools/acme/settings.py new file mode 100644 index 0000000..5c0f246 --- /dev/null +++ b/sicceggetools/acme/settings.py @@ -0,0 +1,29 @@ +#!/usr/bin/python + +import logging +import yaml + + + +class Settings: + def __init__(self, path='config/settings.yaml'): + with open(path) as invfd: + self._settings = yaml.load(invfd.read()) + + + def use_method(self, method, san, settings): + authmethods = self._settings['authorization'] + + for thismethod, data in authmethods.items(): + for host in data['hosts']: + if not san.endswith(host): + continue + + return thismethod == method + + if '*' in authmethods[method]['hosts']: + return True + + return False + + diff --git a/sicceggetools/inventory.py b/sicceggetools/inventory.py new file mode 100644 index 0000000..628d95b --- /dev/null +++ b/sicceggetools/inventory.py @@ -0,0 +1,16 @@ +#!/usr/bin/python + +import logging +import yaml + + +class Inventory: + def __init__(self, path='config/inventory.yaml'): + with open(path) as invfd: + self._inventory = yaml.load(invfd.read()) + + + def get_sans(self, hostname, servicetype, servicename): + servicedict = self._inventory[hostname] + servicesdict = servicedict[servicetype] + return servicesdict[servicename] -- 2.39.5