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()
--- /dev/null
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+# (C) Christoph Egger <christoph@christoph-egger.org>
+
+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()
+++ /dev/null
-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
--- /dev/null
+#!/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
--- /dev/null
+#!/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))
+
--- /dev/null
+#!/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)
DIRECTORY_URL = 'https://acme-v01.api.letsencrypt.org/directory'
-
KEY_SIZE = 4096
-
CONTACT = ('mailto:certmaster@siccegge.de',)
+SERVICETYPES = ('xmpp', 'www', 'smtp', 'imap')
--- /dev/null
+#!/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
+
+
--- /dev/null
+#!/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]