Es hat nicht lange gedauert von der Einrichtung eines Aktualisierungsmechanismus für SSL-/TLS-Zertifikate bis zum Wunsch, auch die Aktualisierung der Zertifikatsfingerabdrücke zu automatisieren.
Im Gegensatz zum openssl-Einzeiler ist die Erzeugung von Fingerprints mit Python nicht ganz trivial. Glücklicherweise gibt es ein großzügig lizenziertes code snippet, auf das ich mich stützen kann. Das Snippet liest den Dateiinhalt im Binärmodus und verwendet die base64-Funktion base64.decodebytes()
zur Umwandlung in das binäre DER-Format. Mit der neueren Funktion base64.b64decode()
lassen sich grundsätzlich Bytestrings und ASCII-Strings umwandeln, dennoch ist der Binärmodus auch für Textdateien die sicherere Wahl:
def get_fingerprint(): lines = [] with open(pem_path, 'rb') as pem: for line in pem: if line.rstrip() == b'-----BEGIN CERTIFICATE-----': continue elif line.rstrip() == b'-----END CERTIFICATE-----': continue else: lines.append(line.strip()) cert = b''.join(lines) der_cert = base64.b64decode(cert) fingerprint = hashlib.sha256(der_cert).hexdigest() fingerprint_upper = fingerprint.upper() fingerprint_colonized = ':'.join(fingerprint_upper[i:i+2] for i in range(0, len(fingerprint_upper), 2)) print(fingerprint_colonized) return fingerprint, fingerprint_colonized
Die Extraktion des eigentlichen Zertifikates ist etwas umständlich, weil zunächst die vornehmlich für Menschen relevanten Begrenzungszeilen zwischen den Elementen der Zertifikatskette und die Zeilenumbrüche entfernt werden müssen. Anschließend wird das Zertifikat binarisiert, es wird ein Hash erzeugt und in hexadezimale Schreibweise gewandelt sowie in eine zusätzliche Doppelpunkt-Notation überführt.
Mit den beiden so erzeugten Zeichenketten können .msmtprc
und .offlineimaprc
aktualisiert werden. Was auf den ersten Blick aussieht wie eine passende Aufgabe für das Tandem fileinput
und re
, erweist sich als etwas schwieriger: Sowohl msmtp als auch offlineimap akzeptieren keine Kommentare am Zeilenende bzw. interpretieren sie als Teil des Parameterwertes. Ich muss die Markierung für die zu aktualisierenden Fingerprints daher in die vorangehende Zeile schreiben, was die Verwendung des zeilenbasierten fileinput
-Moduls ausschließt.
def update_fingerprint_files(fp_msmtp, fp_offlineimap): input = [ (msmtp_path, re.compile('(#auto-updated fingerprint\ntls_fingerprint )(.*)'), fp_msmtp), (offlineimap_path, re.compile('(#auto-updated fingerprint\ncert_fingerprint = )(.*)'), fp_offlineimap) ] results = [] for file, pattern, fingerprint in input: with open(file, 'r') as current_file: content = current_file.read() if not pattern.search(content): result = f'Malformed configuration file! Please check {file} manually.' elif pattern.search(content).group(2) == fingerprint: result = f'File {file} was up to date.' else: result = f'File {file} was updated ("{pattern.search(content).group(2)}" replaced by "{fingerprint}").' updated_content = pattern.sub(rf'\g<1>{fingerprint}', content) current_file.close() with open(file, 'w') as updated_file: updated_file.write(updated_content) print(result) results.append(result) return results
Es wird jeweils die gesamte Konfigurationsdatei (bis zu 121 Zeilen!) eingelesen – an dieser Stelle zahlt sich aus, dass mein aktueller Client mit mehr als 640KB Arbeitsspeicher ausgestattet ist. Enthält die Datei den gesuchten Ausdruck (pattern
), werden der neue und der bereits vorhandene Fingerprint verglichen. Nur bei einer Abweichung wird die gesamte Datei (mit dem neuen Fingerprint) geschrieben. Bemerkenswert ist hier höchstens die Kombination von string literal und format string, mit der ich sowohl ein Element meines Regex-Musters als auch die Variable fingerprint
aufgreifen kann. Der erzielte Effizienzgewinn (Zeitaufwand: 2 Stunden, Einsparung: maximal 3 Minuten alle drei Monate) ist noch niedriger als an anderer Stelle, so dass ich frühestens in 10 Jahren objektiv von meinem Skript profitieren werde. Nicht zu vernachlässigen ist allerdings die unbezahlbare Befriedigung über eine elegante Lösung und natürlich der kollaterale Erkenntnisgewinn.
Update (2022-06-21): Während openssl x509 -noout -fingerprint -sha256 -in $cert
für die von Let's Encrypt bereitgestellten Dateien cert.pem
und fullchain.pem
weiterhin denselben (passenden) Fingerabdruck ausgibt, kann mein Skript eine Zertifikatskette nicht von einem alleinstehenden Zertifikat unterscheiden, so dass ich cert.pem
auswerten muss. Warum im Februar das Skript auch für fullchain.pem
den korrekten Fingerabdruck geliefert hat, ist mysteriös.