Let’s Encrypt
Allgemeines
Let’s Encrypt ist ein neuer Dienst für kostenlose SSL-Zertifikate. Dieses BASH-Script automatisiert das Erstellen von den Keys und Zertifikaten.
Abhänigkeiten
- acme-tiny
- Webserver
- Phyton
- OpenSSL
- NetCat
- Wget
- Cron (Optional)
Funktion
Info folgt noch
Einrichtung
- Als root den folgenden Befehl ausführen:
curl https://dl.thju.de/CryptOn/CryptOn-11.tar | tar -v --absolute-names -x --keep-old-files
- In Das Verzeichnis für die Konfigurationsdateien wechselncd /opt/CryptOn/etc/CryptOn
- Datei /opt/CryptOn/etc/CryptOn/defaults anpassen.
- Für jedes Zertifikat eine .conf datei in /opt/CryptOn/etc/CryptOn/ anlegen
- Script ausführen
/opt/CryptOn-Vorlage/bin/CryptOn - Script in Cron eintragen (täglich ausführen)
- Konfigurationen von Anwendungen, welche dieses Keys / Zertifikate benutzen sollen umstellen. Die Keys liegen in /opt/CryptOn/etc/ssl/(domainname)
Variablen
- CONFDIR: Phat zum Verzeichnis mit Config-Dateien (Optional)
- ACCOUNTKEY: Phat zum Account-Key. Der Key kann mit dem Befehl “openssl genrsa 4096 > account.key” erstellt werden. Dieser Key muss gut gesichert werden. Der Account-Key kann für alle Domains benutzt werden.
- KEYDIR: Hauptverzeichnis für die Generierten Keys.
- DOMAIN: Auf diese Domain wird das Zertifikat ausgestellt (Nur eine Domain zulässig!)
z.B: “DOMAIN=example.com” - ALTDOMAINS: Alternative Domain-Namen (Die DOMAIN wird hier automatisch eingetragen)
z.B: “ALTDOMAINS=”www.example.com example.net www.example.net” - COUNTRY: Kürzel des Landes (DE für Deutschland)*
- STATEORPROVINCENAME: Bundesland*
- CITY: Stadt*
- EMAIL: e-Mail-Adresse*
- WEBROOT: Dieses Verzeichnis muss von den Let’s Encrypt-Server auf Port 80 über HTTP erreichbar sein (Domain und alle ALTDOMAINS!)
- ACMETINY: Vollständiger Phat zu acme_tiny.py
- WEBSERVERRESTARTCMD: Dieses Kommando wird nach erfolgreicher Erstellung von dem Zertifikat ausgeführt. (/bin/true eintragen, wenn nichts ausgeführt werden soll)
- IMAPRESTART: Dieses Kommando wird nach erfolgreicher Erstellung von dem Zertifikat ausgeführt. (/bin/true eintragen, wenn nichts ausgeführt werden soll)
- SMTPRESTART: Dieses Kommando wird nach erfolgreicher Erstellung von dem Zertifikat ausgeführt. (/bin/true eintragen, wenn nichts ausgeführt werden soll)
- CHOWN: Benutzer und Gruppe für Keys werden festgelegt
CHOWN=”root:ssl-cert” für Benutzer root und die Gruppe ssl-cert - CHMOD: Standard CHMOD=”o-rwx” entfernt alle Rechte für Sonstige Benutzer. Dienste, welche diese Keys benötigen müssen Mitglied ind er Gruppe sein.
- ACMESUBDIR: Standard-Unterverzeichnis für die ACME-Challenge (Dieses sollte nicht geändert werden)
- RENEWDAYS:Restlaufzeit des Zertifikates in Tagen, bevor es erneuert wird. Ich Empfehle 15 Tage. Bei 91 Tagen würde das Script bei jedem aufruf das Zeritifikat erneuern
* Diese Angaben werden in den Certificate-Signing-Request (CSR) eingetragen. Let’s Encrypt übernimmt diese vermutlich nicht. Eventuell werden diese Angaben bei einer späteren Version von dem BASH-Script entfernt.
Hinweis: Das Script kann nur 1 Zertifikat erstellen. Werden mehrer Zertifikate benötigt, muss das Script kopiert werden. (Ab Version 06 vom 08.02.2017 werden Config-Dateien unterstützt). Per SAN (ALTDOMAINS) können mehrer Domains in einem Zertifikat eingetragen werden.
TIPP: Für alle Domains den gleichen Challenge-Ordner nehmen und in der Webserver-Config per Alias richtig verknüpfen.
Überprüfen
In dem Ordner $KEYDIR/$DOMAIN/keys liegen die neu erstellen Schlüssel und Zertifikate. Im Fehlerfall bleibt der aktuelle keys Ordner erhalten und die Daten von dem Fehlgeschlagen versuch sind in keys-temp-[datum] zu finden.
In der log.txt kann man sich die ausgaben von der Erstell-Prozess anschauen. Das Script schreibt alle ausgaben in in log.txt. Eine Ausgabe auf die Konsole findet nicht statt.
Mit dem Befehl “openssl req -noout -text -in example.com.csr” kann man sich den Inhalt von dem Certificate-Signing-Request anschauen.
Mit dem Befehl “openssl x509 -noout -text -in example.com.crt” kann man sich den Inhalt von dem Zertifikat anschauen.
Config-Dateien
Beispiel von einer “defaults” Config
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
#Default für alle Domains #Diese Datei muss im CONFDIR mit dem namen defaults liegen. ACCOUNTKEY="/opt/CryptOn/etc/ssl/Account-Key/Account-Key.key" KEYDIR="/opt/CryptOn/etc/ssl" COUNTRY="DE" STATEORPROVINCENAME="Baden-Wuertemberg" CITY="Bruchsal" EMAIL="info@example.com" #WEBROOT: Verzeichnis für die ACME-Challenge (Port 80!!!, nicht 443!!!) WEBROOT="/var/www/CryptOn" ACMETINY="/opt/CryptOn/bin/acme_tiny.py" WEBSERVERRESTARTCMD="/bin/true" IMAPRESTART="/bin/true" SMTPRESTART="/bin/true" CHOWN="root:ssl-cert" RENEWDAYS="15" ADD127001HOSTS="false" BITS="4096" PYTHON="/usr/bin/python3" |
Beispiel von einer “example.conf”
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
#Konfiguration für ein Zertifikat #Diese Datei muss im CONFDIR liegen und die Endung .conf haben. DOMAIN="example.com" ALTDOMAINS="www.example.com subdom1.example.com subdom2.example.com" #Bei Bedarf können die Restart-Kommandos angepasst und auskommentiert werden #WEBSERVERRESTARTCMD="/bin/true" #IMAPRESTART="/bin/true" #SMTPRESTART="/bin/true" #Nur nötig, wenn kein Alias auf dem Webserver eingerichtet ird. #WEBROOT="/var/www/example" #Für Langesame Geräte welche mit 4096 Bit-Keys überlastet sind #BITS="2048" #Fügt der Host-Datei 127.0.0.1 für die Angefragten Domains hinzu. #Ist für spezielle Konfigurationen hinter NAT-Routern. #ADD127001HOSTS="false" |
Code / Script
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 |
#!/bin/bash #Name: CryptOn, a ACME Client for "Let's Encrypt" #Description: Erstellt SSL-Keys für et's Encrypt, Es wird jedesmal ein neuer Secret-Key generiert! #Dependencies: acme_tiny.py, mkdir, wget, python, openssl, webserver, chown, chmod, cat, nc (netcat), let, cut, date #Author: Thomas Jungbauer #License: WTFPL #License-Text-Url: http://sam.zoy.org/wtfpl/COPYING #Version: 11 #Relase-Date: 02.01.2016 #Last-change-Date: 01.01.2021 # Hinweis: Der Account Key einmalig erstellt. Dieser muss gut gesichtert werden. # Der Key wird in dem Script mit dem folgendem Befehl erstellt: # openssl genrsa $BITS > account.key # Mit diesem Befehle kann man sich den Public Key Anzeigen lassen # openssl rsa -in account.key -pubout # Hinweis - ACME-Challange # Let's Encrypt validiert die domain mithilfe des Webservers # $WEBROOT muss unter http://$DOMAIN erreichbar sein # Alle alternativen Domains müssen auch über den Webserver erreichbar sein #Benutzer Variablen für Einstellungen CONFDIR="/opt/CryptOn/etc/CryptOn" ACCOUNTKEY="/opt/CryptOn/etc/ssl/Account-Key/Account-Key.key" KEYDIR="/opt/CryptOn/etc/ssl" DOMAIN="example.com" ALTDOMAINS="www.example.com subdom1.example.com subdom2.example.com" COUNTRY="DE" STATEORPROVINCENAME="Baden-Wuertemberg" CITY="Bruchsal" EMAIL="info@example.com" #Verzeichnis für die ACME-Challenge (Port 80!!!, nicht 443!!!) WEBROOT="/var/www/example" ACMETINY="/opt/CryptOn/bin/acme_tiny.py" PYTHON="/usr/bin/python3" WEBSERVERRESTARTCMD="/etc/init.d/apache2 restart" IMAPRESTART="/bin/true" SMTPRESTART="/bin/true" CHOWN="root:ssl-cert" RENEWDAYS="15" #Tage bevor der Key Expired wird er erneuert ADD127001HOSTS="false" BITS="4096" #Ende der Benutzer-Variablen. #Weitere Vaiablen (Diese müssen im Normallfall nicht geändert werden) CHMOD="o-rwx" LOG="$KEYDIR/$DOMAIN/keys-tmp/log.txt" #Standard-Unterverzeichnis für die ACME-Challenge (Port 80!!!, nicht 443!!!) ACMESUBDIR=".well-known/acme-challenge/" HOSTS="/etc/hosts" #Ab hier beginnt das Script. makecert() { #Wird auf false gesetzt, wenn was schief läuft CRTOK=true #Es wird geprüft, ob das Cert erneuert werden muss. if [ -f $KEYDIR/$DOMAIN/keys/$DOMAIN.crt ]; then END=`openssl x509 -noout -enddate -in $KEYDIR/$DOMAIN/keys/$DOMAIN.crt |cut -c 10-` ENDSEC=`date --date="$END" +%s` let RENEWSEC=24*60*60*$RENEWDAYS+`date +%s` if [ $ENDSEC -gt $RENEWSEC ]; then #echo "Certifikat von $DOMAIN ist noch gültig" return 0 fi fi #Es wird geprüft, ob der ACCOUNT-Key existiert. if [ ! -f $ACCOUNTKEY ]; then echo "Accont Key $ACCOUNTKEY nicht vorhanden" echo "Key wird erstellt" openssl genrsa $BITS > $ACCOUNTKEY fi #Es wird geprüft, ob acme-tiny vorhanden ist if [ ! -f $ACMETINY ]; then echo "ACME_TINY nicht gefunden" echo "URL: https://github.com/diafygi/acme-tiny" exit 1 fi #Temp-Ordner für Keys werden erstellt if [ ! -d $KEYDIR/$DOMAIN/keys-tmp ]; then mkdir -p $KEYDIR/$DOMAIN/keys-tmp else rm $KEYDIR/$DOMAIN/keys-tmp $KEYDIR/$DOMAIN/keys-tmp-`date +%Y-%m-%d-%H-%M` mkdir -p $KEYDIR/$DOMAIN/keys-tmp fi #Datum und Variablen werden ins LOG geschrieben echo "--- Start ---" >>$LOG date >>$LOG echo "Domain: $DOMAIN" >>$LOG echo "Accountkey: $ACCOUNTKEY" >>$LOG echo "------------------------" >>$LOG #/etc/hosts wird um 127.0.0.1 $DOMAIN $ALTDOMAINS ergänzt if [ $ADD127001HOSTS = true ]; then cp --preserve=all $HOSTS $HOSTS-CryptOn-Backup echo "127.0.0.1 $DOMAIN $ALTDOMAINS" >> $HOSTS fi #Es wird geprüft, ob die Domain erreichbar ist if nc -z $DOMAIN 80; then echo "Domain erreichbar" >>$LOG else echo "Domain nicht erreichbar" >>$LOG if [ $ADD127001HOSTS = true ]; then #/etc/host wird in den Ursprünglichen zustand versetzt rm -I $HOSTS mv $HOSTS-CryptOn-Backup $HOSTS fi exit 1 fi #ACME-Challenge Verzeichniss wird erstellt if [ ! -d $WEBROOT/$ACMESUBDIR ]; then mkdir -p $WEBROOT/$ACMESUBDIR fi #Private Key wird erstellt openssl genrsa $BITS > $KEYDIR/$DOMAIN/keys-tmp/$DOMAIN.key 2>>$LOG #Create a Strong DH Group (dhparams.pem) #Quelle: https://weakdh.org/sysadmin.html openssl dhparam -out $KEYDIR/$DOMAIN/keys-tmp/dhparams-2048.pem 2048 openssl dhparam -out $KEYDIR/$DOMAIN/keys-tmp/dhparams-4096.pem 4096 #Config-Datei für CSR wird erstellt echo "[ req ] distinguished_name = req_distinguished_name string_mask = nombstr # The extensions to add to a certificate request req_extensions = v3_req # GWDG default options for certificate request [ req_distinguished_name ] countryName = Country Name (2 letter code)" > $KEYDIR/$DOMAIN/keys-tmp/$DOMAIN.cnf echo -n "countryName_default = " >> $KEYDIR/$DOMAIN/keys-tmp/$DOMAIN.cnf echo $COUNTRY >> $KEYDIR/$DOMAIN/keys-tmp/$DOMAIN.cnf echo "countryName_min = 2" >> $KEYDIR/$DOMAIN/keys-tmp/$DOMAIN.cnf echo "countryName_max = 2" >> $KEYDIR/$DOMAIN/keys-tmp/$DOMAIN.cnf echo "stateOrProvinceName = State or Province Name (full name)" >> $KEYDIR/$DOMAIN/keys-tmp/$DOMAIN.cnf echo -n "stateOrProvinceName_default = " >> $KEYDIR/$DOMAIN/keys-tmp/$DOMAIN.cnf echo $STATEORPROVINCENAME >> $KEYDIR/$DOMAIN/keys-tmp/$DOMAIN.cnf echo "localityName = Your City" >> $KEYDIR/$DOMAIN/keys-tmp/$DOMAIN.cnf echo -n "localityName_default = " >> $KEYDIR/$DOMAIN/keys-tmp/$DOMAIN.cnf echo $CITY >> $KEYDIR/$DOMAIN/keys-tmp/$DOMAIN.cnf echo "commonName = YOUR NAME" >> $KEYDIR/$DOMAIN/keys-tmp/$DOMAIN.cnf echo "commonName_max = 64" >> $KEYDIR/$DOMAIN/keys-tmp/$DOMAIN.cnf echo -n "commonName_default = " >> $KEYDIR/$DOMAIN/keys-tmp/$DOMAIN.cnf echo $DOMAIN >> $KEYDIR/$DOMAIN/keys-tmp/$DOMAIN.cnf echo "emailAddress = E-MAIL" >> $KEYDIR/$DOMAIN/keys-tmp/$DOMAIN.cnf echo "emailAddress_max = 64" >> $KEYDIR/$DOMAIN/keys-tmp/$DOMAIN.cnf echo -n "emailAddress_default = " >> $KEYDIR/$DOMAIN/keys-tmp/$DOMAIN.cnf echo $EMAIL >> $KEYDIR/$DOMAIN/keys-tmp/$DOMAIN.cnf echo "[ v3_req ]" >> $KEYDIR/$DOMAIN/keys-tmp/$DOMAIN.cnf echo -n "subjectAltName = DNS:" >> $KEYDIR/$DOMAIN/keys-tmp/$DOMAIN.cnf echo -n $DOMAIN >> $KEYDIR/$DOMAIN/keys-tmp/$DOMAIN.cnf for SUB in $ALTDOMAINS; do echo -n ", DNS:" >> $KEYDIR/$DOMAIN/keys-tmp/$DOMAIN.cnf echo -n $SUB >> $KEYDIR/$DOMAIN/keys-tmp/$DOMAIN.cnf done #CSR wird erstellt openssl req -new -sha256 -key $KEYDIR/$DOMAIN/keys-tmp/$DOMAIN.key -batch -config $KEYDIR/$DOMAIN/keys-tmp/$DOMAIN.cnf > $KEYDIR/$DOMAIN/keys-tmp/$DOMAIN.csr #CSR von Lets-Encrypt signieren lassen $PYTHON $ACMETINY --account-key $ACCOUNTKEY --csr $KEYDIR/$DOMAIN/keys-tmp/$DOMAIN.csr --acme-dir $WEBROOT/$ACMESUBDIR/ > $KEYDIR/$DOMAIN/keys-tmp/$DOMAIN.crt 2>>$LOG #/etc/hosts wird wieder in den Orginal-Zustand gebracht. if [ $ADD127001HOSTS = true ]; then #/etc/host wird in den Ursprünglichen zustand versetzt rm -I $HOSTS mv $HOSTS-CryptOn-Backup $HOSTS fi # Append the Let's Encrypt intermediate cert to your cert wget -O - https://letsencrypt.org/certs/lets-encrypt-r3.pem > $KEYDIR/$DOMAIN/keys-tmp/lets-encrypt-r3.pem 2>>$LOG wget -O - https://letsencrypt.org/certs/lets-encrypt-r3-cross-signed.pem > $KEYDIR/$DOMAIN/keys-tmp/lets-encrypt-r3-cross-signed.pem 2>>$LOG wget -O - https://letsencrypt.org/certs/lets-encrypt-e1.pem > $KEYDIR/$DOMAIN/keys-tmp/lets-encrypt-e1.pem 2>>$LOG # Check if the Let's Encrypt intermediate cert already here. (A newer Version of acme_tiny.py add this by istself) if [ `grep -c CERTIFICATE $KEYDIR/$DOMAIN/keys-tmp/$DOMAIN.crt` = 2 ]; then cat $KEYDIR/$DOMAIN/keys-tmp/lets-encrypt-r3.pem >>$KEYDIR/$DOMAIN/keys-tmp/$DOMAIN.crt cat $KEYDIR/$DOMAIN/keys-tmp/lets-encrypt-r3-cross-signed.pem >>$KEYDIR/$DOMAIN/keys-tmp/$DOMAIN.crt cat $KEYDIR/$DOMAIN/keys-tmp/lets-encrypt-e1.pem >>$KEYDIR/$DOMAIN/keys-tmp/$DOMAIN.crt fi #Jetzt kommen die Prüfungen, ob das CRT OK ist. if [ `grep -c CERTIFICATE $KEYDIR/$DOMAIN/keys-tmp/$DOMAIN.crt` -ge 4 ]; then echo "Zertifikat ist OK" >>$LOG #PEM-Datei wird erstellt (PEM = KEY + CRT) cat $KEYDIR/$DOMAIN/keys-tmp/$DOMAIN.key >$KEYDIR/$DOMAIN/keys-tmp/$DOMAIN.pem cat $KEYDIR/$DOMAIN/keys-tmp/$DOMAIN.crt >>$KEYDIR/$DOMAIN/keys-tmp/$DOMAIN.pem #Key in DER-Format convertieren openssl pkey -inform pem -in $KEYDIR/$DOMAIN/keys-tmp/$DOMAIN.key -outform der -out $KEYDIR/$DOMAIN/keys-tmp/$DOMAIN.DER.key openssl x509 -inform pem -in $KEYDIR/$DOMAIN/keys-tmp/$DOMAIN.crt -outform der -out $KEYDIR/$DOMAIN/keys-tmp/$DOMAIN.DER.crt else CRTOK=false echo "Zertifikat ist Fehlerhaft" >>$LOG fi #Rechte chown $CHOWN -R $KEYDIR/$DOMAIN chmod $CHMOD -R $KEYDIR/$DOMAIN #Hier wird anhand des Prüfungsergebnisses weiter entschieden if $CRTOK; then #CRT ist OK #Ordner für Keys werden erstellt if [ ! -d $KEYDIR/$DOMAIN/backups ]; then mkdir -p $KEYDIR/$DOMAIN/backups fi if [ ! -d $KEYDIR/$DOMAIN/keys ]; then mv $KEYDIR/$DOMAIN/keys-tmp $KEYDIR/$DOMAIN/keys else mv $KEYDIR/$DOMAIN/keys $KEYDIR/$DOMAIN/backups/`date +%Y-%m-%d-%H-%M` mv $KEYDIR/$DOMAIN/keys-tmp $KEYDIR/$DOMAIN/keys fi #Webserver neu Starten! $WEBSERVERRESTARTCMD 2>/dev/null >/dev/null #Mailserver neu Starten! $IMAPRESTART 2>/dev/null >/dev/null $SMTPRESTART 2>/dev/null >/dev/null else #Fehler beim CRT mv $KEYDIR/$DOMAIN/keys-tmp $KEYDIR/$DOMAIN/keys-tmp-failed-`date +%Y-%m-%d-%H-%M` exit 1 fi } for CONFFILE in "$CONFDIR"/*.conf do if [ -f $CONFFILE ]; then #Falls vorhanden wird die "defaults" geladen if [ -f $CONFDIR/defaults ] ; then source $CONFDIR/defaults fi #Config-Datei (*.conf) wird geladen source $CONFFILE LOG="$KEYDIR/$DOMAIN/keys-tmp/log.txt" makecert else #Keine Config vorhanden, Script wird mit Vorgabe-Werten ausgeführt makecert fi done exit 0 |
acme-tiny von Github
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 |
#!/usr/bin/env python3 # Copyright Daniel Roesler, under MIT license, see LICENSE at github.com/diafygi/acme-tiny import argparse, subprocess, json, os, sys, base64, binascii, time, hashlib, re, copy, textwrap, logging try: from urllib.request import urlopen, Request # Python 3 except ImportError: # pragma: no cover from urllib2 import urlopen, Request # Python 2 DEFAULT_CA = "https://acme-v02.api.letsencrypt.org" # DEPRECATED! USE DEFAULT_DIRECTORY_URL INSTEAD DEFAULT_DIRECTORY_URL = "https://acme-v02.api.letsencrypt.org/directory" LOGGER = logging.getLogger(__name__) LOGGER.addHandler(logging.StreamHandler()) LOGGER.setLevel(logging.INFO) def get_crt(account_key, csr, acme_dir, log=LOGGER, CA=DEFAULT_CA, disable_check=False, directory_url=DEFAULT_DIRECTORY_URL, contact=None, check_port=None): directory, acct_headers, alg, jwk = None, None, None, None # global variables # helper functions - base64 encode for jose spec def _b64(b): return base64.urlsafe_b64encode(b).decode('utf8').replace("=", "") # helper function - run external commands def _cmd(cmd_list, stdin=None, cmd_input=None, err_msg="Command Line Error"): proc = subprocess.Popen(cmd_list, stdin=stdin, stdout=subprocess.PIPE, stderr=subprocess.PIPE) out, err = proc.communicate(cmd_input) if proc.returncode != 0: raise IOError("{0}\n{1}".format(err_msg, err)) return out # helper function - make request and automatically parse json response def _do_request(url, data=None, err_msg="Error", depth=0): try: resp = urlopen(Request(url, data=data, headers={"Content-Type": "application/jose+json", "User-Agent": "acme-tiny"})) resp_data, code, headers = resp.read().decode("utf8"), resp.getcode(), resp.headers except IOError as e: resp_data = e.read().decode("utf8") if hasattr(e, "read") else str(e) code, headers = getattr(e, "code", None), {} try: resp_data = json.loads(resp_data) # try to parse json results except ValueError: pass # ignore json parsing errors if depth < 100 and code == 400 and resp_data['type'] == "urn:ietf:params:acme:error:badNonce": raise IndexError(resp_data) # allow 100 retrys for bad nonces if code not in [200, 201, 204]: raise ValueError("{0}:\nUrl: {1}\nData: {2}\nResponse Code: {3}\nResponse: {4}".format(err_msg, url, data, code, resp_data)) return resp_data, code, headers # helper function - make signed requests def _send_signed_request(url, payload, err_msg, depth=0): payload64 = "" if payload is None else _b64(json.dumps(payload).encode('utf8')) new_nonce = _do_request(directory['newNonce'])[2]['Replay-Nonce'] protected = {"url": url, "alg": alg, "nonce": new_nonce} protected.update({"jwk": jwk} if acct_headers is None else {"kid": acct_headers['Location']}) protected64 = _b64(json.dumps(protected).encode('utf8')) protected_input = "{0}.{1}".format(protected64, payload64).encode('utf8') out = _cmd(["openssl", "dgst", "-sha256", "-sign", account_key], stdin=subprocess.PIPE, cmd_input=protected_input, err_msg="OpenSSL Error") data = json.dumps({"protected": protected64, "payload": payload64, "signature": _b64(out)}) try: return _do_request(url, data=data.encode('utf8'), err_msg=err_msg, depth=depth) except IndexError: # retry bad nonces (they raise IndexError) return _send_signed_request(url, payload, err_msg, depth=(depth + 1)) # helper function - poll until complete def _poll_until_not(url, pending_statuses, err_msg): result, t0 = None, time.time() while result is None or result['status'] in pending_statuses: assert (time.time() - t0 < 3600), "Polling timeout" # 1 hour timeout time.sleep(0 if result is None else 2) result, _, _ = _send_signed_request(url, None, err_msg) return result # parse account key to get public key log.info("Parsing account key...") out = _cmd(["openssl", "rsa", "-in", account_key, "-noout", "-text"], err_msg="OpenSSL Error") pub_pattern = r"modulus:[\s]+?00:([a-f0-9\:\s]+?)\npublicExponent: ([0-9]+)" pub_hex, pub_exp = re.search(pub_pattern, out.decode('utf8'), re.MULTILINE|re.DOTALL).groups() pub_exp = "{0:x}".format(int(pub_exp)) pub_exp = "0{0}".format(pub_exp) if len(pub_exp) % 2 else pub_exp alg, jwk = "RS256", { "e": _b64(binascii.unhexlify(pub_exp.encode("utf-8"))), "kty": "RSA", "n": _b64(binascii.unhexlify(re.sub(r"(\s|:)", "", pub_hex).encode("utf-8"))), } accountkey_json = json.dumps(jwk, sort_keys=True, separators=(',', ':')) thumbprint = _b64(hashlib.sha256(accountkey_json.encode('utf8')).digest()) # find domains log.info("Parsing CSR...") out = _cmd(["openssl", "req", "-in", csr, "-noout", "-text"], err_msg="Error loading {0}".format(csr)) domains = set([]) common_name = re.search(r"Subject:.*? CN\s?=\s?([^\s,;/]+)", out.decode('utf8')) if common_name is not None: domains.add(common_name.group(1)) subject_alt_names = re.search(r"X509v3 Subject Alternative Name: (?:critical)?\n +([^\n]+)\n", out.decode('utf8'), re.MULTILINE|re.DOTALL) if subject_alt_names is not None: for san in subject_alt_names.group(1).split(", "): if san.startswith("DNS:"): domains.add(san[4:]) log.info(u"Found domains: {0}".format(", ".join(domains))) # get the ACME directory of urls log.info("Getting directory...") directory_url = CA + "/directory" if CA != DEFAULT_CA else directory_url # backwards compatibility with deprecated CA kwarg directory, _, _ = _do_request(directory_url, err_msg="Error getting directory") log.info("Directory found!") # create account, update contact details (if any), and set the global key identifier log.info("Registering account...") reg_payload = {"termsOfServiceAgreed": True} if contact is None else {"termsOfServiceAgreed": True, "contact": contact} account, code, acct_headers = _send_signed_request(directory['newAccount'], reg_payload, "Error registering") log.info("{0} Account ID: {1}".format("Registered!" if code == 201 else "Already registered!", acct_headers['Location'])) if contact is not None: account, _, _ = _send_signed_request(acct_headers['Location'], {"contact": contact}, "Error updating contact details") log.info("Updated contact details:\n{0}".format("\n".join(account['contact']))) # create a new order log.info("Creating new order...") order_payload = {"identifiers": [{"type": "dns", "value": d} for d in domains]} order, _, order_headers = _send_signed_request(directory['newOrder'], order_payload, "Error creating new order") log.info("Order created!") # get the authorizations that need to be completed for auth_url in order['authorizations']: authorization, _, _ = _send_signed_request(auth_url, None, "Error getting challenges") domain = authorization['identifier']['value'] # skip if already valid if authorization['status'] == "valid": log.info("Already verified: {0}, skipping...".format(domain)) continue log.info("Verifying {0}...".format(domain)) # find the http-01 challenge and write the challenge file challenge = [c for c in authorization['challenges'] if c['type'] == "http-01"][0] token = re.sub(r"[^A-Za-z0-9_\-]", "_", challenge['token']) keyauthorization = "{0}.{1}".format(token, thumbprint) wellknown_path = os.path.join(acme_dir, token) with open(wellknown_path, "w") as wellknown_file: wellknown_file.write(keyauthorization) # check that the file is in place try: wellknown_url = "http://{0}{1}/.well-known/acme-challenge/{2}".format(domain, "" if check_port is None else ":{0}".format(check_port), token) assert (disable_check or _do_request(wellknown_url)[0] == keyauthorization) except (AssertionError, ValueError) as e: raise ValueError("Wrote file to {0}, but couldn't download {1}: {2}".format(wellknown_path, wellknown_url, e)) # say the challenge is done _send_signed_request(challenge['url'], {}, "Error submitting challenges: {0}".format(domain)) authorization = _poll_until_not(auth_url, ["pending"], "Error checking challenge status for {0}".format(domain)) if authorization['status'] != "valid": raise ValueError("Challenge did not pass for {0}: {1}".format(domain, authorization)) os.remove(wellknown_path) log.info("{0} verified!".format(domain)) # finalize the order with the csr log.info("Signing certificate...") csr_der = _cmd(["openssl", "req", "-in", csr, "-outform", "DER"], err_msg="DER Export Error") _send_signed_request(order['finalize'], {"csr": _b64(csr_der)}, "Error finalizing order") # poll the order to monitor when it's done order = _poll_until_not(order_headers['Location'], ["pending", "processing"], "Error checking order status") if order['status'] != "valid": raise ValueError("Order failed: {0}".format(order)) # download the certificate certificate_pem, _, _ = _send_signed_request(order['certificate'], None, "Certificate download failed") log.info("Certificate signed!") return certificate_pem def main(argv=None): parser = argparse.ArgumentParser( formatter_class=argparse.RawDescriptionHelpFormatter, description=textwrap.dedent("""\ This script automates the process of getting a signed TLS certificate from Let's Encrypt using the ACME protocol. It will need to be run on your server and have access to your private account key, so PLEASE READ THROUGH IT! It's only ~200 lines, so it won't take long. Example Usage: python acme_tiny.py --account-key ./account.key --csr ./domain.csr --acme-dir /usr/share/nginx/html/.well-known/acme-challenge/ > signed_chain.crt """) ) parser.add_argument("--account-key", required=True, help="path to your Let's Encrypt account private key") parser.add_argument("--csr", required=True, help="path to your certificate signing request") parser.add_argument("--acme-dir", required=True, help="path to the .well-known/acme-challenge/ directory") parser.add_argument("--quiet", action="store_const", const=logging.ERROR, help="suppress output except for errors") parser.add_argument("--disable-check", default=False, action="store_true", help="disable checking if the challenge file is hosted correctly before telling the CA") parser.add_argument("--directory-url", default=DEFAULT_DIRECTORY_URL, help="certificate authority directory url, default is Let's Encrypt") parser.add_argument("--ca", default=DEFAULT_CA, help="DEPRECATED! USE --directory-url INSTEAD!") parser.add_argument("--contact", metavar="CONTACT", default=None, nargs="*", help="Contact details (e.g. mailto:aaa@bbb.com) for your account-key") parser.add_argument("--check-port", metavar="PORT", default=None, help="what port to use when self-checking the challenge file, default is port 80") args = parser.parse_args(argv) LOGGER.setLevel(args.quiet or LOGGER.level) signed_crt = get_crt(args.account_key, args.csr, args.acme_dir, log=LOGGER, CA=args.ca, disable_check=args.disable_check, directory_url=args.directory_url, contact=args.contact, check_port=args.check_port) sys.stdout.write(signed_crt) if __name__ == "__main__": # pragma: no cover main(sys.argv[1:]) |
Zukünftige mögliche Verbesserungen
Unterstützung von Config-Dateien.Seit Version 06 vom 08.02.2017 vorhandenÜberprüfung des Ablaufsdatum eines Zertifikates.Seit Version 06 vom 08.02.2017 vorhanden
Changelog
Version 11 vom 01.01.2021
- Let’s Encrypt hat die Chain of Trust geändert. Neue Zwischenzertifikate eingebaut.
Version 10 vom 05.01.2019
- Bug mit neuer acme_tiny.py Version gefixed
- Schlüssellänge (BITS) sind pro Domain Einstellbar
- In der /etc/hosts kann 127.0.0.1 für die Angefragten Domains hinzugefügt werden. Ist für Spezielle Anforderungen hinter NAT-Routern
Version 08 vom 27.05.2017
- Installation vereinfacht
- Pfade für Dateien angepasst. Es liegen jetzt nach dem Entpacken alle Dateien in /opt/CryptOn.
- Account Key wird jetzt bei Bedarf erstellt
- Name in CryptOn geändert
Version 07 vom 22.03.2017
- Rechte für *.pem Datei wurden nicht gesetzt
Version 06 vom 08.02.2017
- Unterstützung für Konfigurations-Dateien hinzugefügt
- Prüfung der Restlaufzeit von dem Zertifikat hinzugefügt
Version 05 vom 14.04.2016
- Zwischenzertifikat vom x1 aus x3 umgestellt
Version 04 vom 04.02.2016
- Erste Veröffentlichung
Rückmeldung (Feedback)
Rückmeldung als Kommentar oder e-Mail (siehe Impressum) ist willkommen.