Zur Kommunikation mit Services unserer Partner verwenden wir häufig Client-Zertifikate inkl. passender Zertifikats-Chain. Client-Zertifikate sind üblicherweise für zwei Jahre gültig, was letztlich regelmäßige Aktualisierungen notwendig macht. Die Zertifikate werden stellenweise zur Authentifizierung am Service benutzt.

Ein sauberer Umgang mit den Zertifikaten ist daher notwendig, doch fühlt sich das ganze manchmal etwas fummelig und fehleranfällig an. Das Gefummel lässt sich nicht einfach so wegoptimieren. Die Vermeidung von Fehlern kann jedoch durch besseres Verständnis der einzelnen Puzzleteile erreicht werden. In diesem Artikel soll deshalb das Puzzle Mutual TLS mit einigen Einzelteilen für unseren Kontext beschrieben werden.

Vertrauen zwischen Client und Server

Um im Browser oder in Java-Bibliotheken HTTP mit TLS zu verschlüsseln und den Mechanismus zur Prüfung der Client-/Server-Zertifikate zu aktivieren, verwenden wir HTTPS. Der Client initiiert die Verbindung zum Server, beispielsweise unter https://www.example.com. Nach dem üblichen TCP-Vorgeplänkel verlangt der Client vom Server dessen Zertifikat, das der Server inklusive seines Public Keys zurückschickt.

Der Client kann nun das Server-Zertifikat validieren. Dazu vergleicht der Client, ob das im Zertifikat definierte Subject ihm bekannt ist, das Zertifikat zur angefragen Domain gehört und ob mit Hilfe des Public Keys des Issuers die Signatur verifiziert werden kann.

Oh, das war etwas viel auf einmal. Nochmal langsam zum Inhalt des Zertifikats: die durch das Zertifikat ausgewiesene Entität (der Server mit dessen Domainname) ist das Subject des Zertifikates, während der Aussteller bzw. die Signatur als Issuer deklariert ist. Sowohl Subject als auch Issuer werden mit Hilfe verschiedener Attribute beschrieben, die in ihrer Gesamtheit als Distinguished Name (DN) bezeichnet werden. Bei Webservern enthält das Attribut Common Name (CN) den Domainnamen. Aliase der Domain werden optional in anderen Bereichen des Zertifikats abgelegt.

Die Zertifikate lassen sich im Browser einsehen, oder auch auf der Kommandozeile per openssl s_client ausgeben:

openssl s_client -showcerts -connect example.com:443
<CTRL+c>

Für example.com sieht das Subject wie folgt aus:

/C=US/ST=California/L=Los Angeles/O=Internet Corporation for Assigned Names and Numbers/OU=Technology/CN=www.example.org

Um dieser Information zu vertrauen zu können, muss der Client dem Issuer vertrauen. Der Issuer lautet im Beispiel:

/C=US/O=DigiCert Inc/OU=www.digicert.com/CN=DigiCert SHA2 High Assurance Server CA

Das Zertifikat muss also mit dem Public Key von DigiCert SHA2 High Assurance Server CA verifizierbar sein. Der Client kennt dazu eine gewisse Menge von Issuern, die auch als Certificate Authority (CA) bekannt sind. Kennen bedeutet implizit auch Vertrauen und äußert sich darin, dass die Zertifikate lokal vorliegen. Letztlich basiert also der komplette Mechanismus um Zertifikatsverifizierung auf Trust. Betriebssystem-, Browserhersteller und sonstige Paket-Maintainer kümmern sich mittels Updatemechanismen darum, dass keinen ungültigen Zertifikaten vertraut wird und dass auch geänderte und neue Zertifikate von offiziellen CAs zeitnah auf allen Systemen verfügbar sind.

Wenn der Client nun in seiner vertrauenswürdigen Menge von Zertifikaten und Public Keys den oben genannten Issuer findet, dann kann die Signatur des Server-Zertifikats überprüft werden und somit dem Inhalt vertraut werden.

Zertifikats-Chains

Neben den Server-Zertifikaten, haben auch die Issuer-Zertifikate eine begrenzte Gültigkeitsdauer. Um die bei Ablauf von Zertifikaten notwendigen Updates weniger häufig durchführen zu müssen, ist die Gültigkeit meist etwas länger gewählt. Dennoch sollte die Rotation häufig genug erfolgen, es entsteht also ein Spannungsfeld zwischen Minimierung der Updatefrequenz und einem zeitnahen Ablauf von Zertifikaten. Um diese Spannung zu adressieren, verteilt man typischerweise nicht die direkten Issuer-Zertifkate an alle Systeme, sondern verlängert die dabei entstehende Kette um einen oder mehrere Issuer. Der Issuer I1 des Server-Zertifikats wird also selbst zum Subject, dessen Zertifikat durch einen weiteren Issuer I2 unterschrieben wird.

Das Zertifikat für I2 kann nun eine längere Gültigkeit erhalten und wird an alle Systeme verteilt, während das Zertifikat für I1 eine kurze Gültigkeit erhält. Der Client muss I1 nun nicht mehr direkt vertrauen, sondern kann mittels I2 die komplette Zertifikats-Kette verifizieren.

Genau das ist im Beispiel sogar der Fall: das Zertifikat für den oben genannten Issuer ist nämlich durch folgenden Issuer ausgestellt:

/C=US/O=DigiCert Inc/OU=www.digicert.com/CN=DigiCert High Assurance EV Root CA

Das im CN enthaltene Infix Root ist bereits der Hinweis, dass die Wurzel der Kette erreicht ist und das oben genannte Zertifikat von I1 wird zum sogenannten Intermediate Certificate. Der Client sollte nun entweder lokal das passende Zertifikat als vertrauenswürdig einstufen, oder es muss manuell von digicert.com heruntergeladen und als vertrauenswürdig eingerichtet werden… oder man bemerkt, dass der Kette eben nicht vertraut werden kann und somit auch die Verbindung zu example.com inhaltlich eher fragwürdig wird.

Mutial TLS und Client-Authentifizierung

Zurück zu genau dieser Verbindung, die der Client mit dem Server aufbauen wollte: Der Server kann im Rahmen des Verbindungsaufbaus auch selbst einen Certificate Request stellen. Dieser enthält einige Namen von im Server vertrauten CAs und weist den Client an, sich selbst gegenüber dem Server per Zertifikat auszuweisen.

Der Client muss deshalb seinerseits ein Zertifikat inklusive der dazugehörigen Issuer-Chain an den Server senden. Die Chain muss wenigstens mit ihrem letzten Element zu einem der im Certificate Request geforderten CAs passen, damit der Server die gesamte Chain bis hin zum Client-Zertifikat verifizieren kann. Wenn sowohl Client als auch Server sich vertrauen, erfolgt der Rest des TLS-Protokolls, in dem Secrets und Session Keys ausgetauscht werden, mit deren Hilfe letztlich die weitere Kommunikation verschlüsselt stattfinden kann.

Optional kann im Rahmen der Client-Verifizierung am Server auch eine Authentifizierung stattfinden. Dazu pflegt der Server lediglich im LDAP oder einer anderen Datenbank einen passenden Eintrag zum Client-Zertifikat bzw. zu dessen DN.

Die folgende Grafik stellt den beschriebenen Ausschnitt aus dem TLS Handshake dar:

Mutual-TLS (Ausschnitt)

Bevor es mit den in der Praxis relevanten Aspekten weitergeht folgt erst eine kleine Zusammenfassung zum bisher beschriebenen Ablauf:

  • Server- und Client-Zertifikate werden einem Subject von einem Issuer ausgestellt und signiert. Ein Zertifikat enthält den Public Key des Subjects.
  • Ein Issuer drückt durch seine Signatur aus, dass er dem Subjekt und dessen im Zertifikat enthaltenen Eigenschaften vertraut. Dieses Vertrauen wird in der Regel in einem unabhängigen Prozess geprüft, bevor die Signatur erstellt wird.
  • Um einem Zertifikat zu vertrauen, muss dessen Issuer vertraut werden (Oder dessen Issuer. Oder dessen Issuer [sic]).
  • Vertrauen wird hergestellt, indem ein Zertifikat mit einer wohldefinierten Menge von vertrauenswürdigen Zertifikaten abgeglichen wird. Bei Webservern muss außerdem das Zertifikat zum angesprochenen Hostname gehören.
  • Zertifikats-Chains erlauben mittels Indirektion einfache Update-Mechanismen. Eine Chain ist vertrauenswürdig, wenn die einzelnen Elemente mit ihren jeweiligen Verweisen auf die beteiligten Issuer vertrauenswürdig sind und dem Root-Zertifikat vertraut wird.
  • Root CAs signieren ihre Zertifikate selbst.

Das Gefummel mit Zertifikats-Chains

In der Praxis gibt es selten Chains mit weniger als drei Elementen (Server-/Client-, Intermediary-, Root-Zertifikat), es reicht also nicht, sich nur mit dem “Blatt-Zertifikat” zu beschäftigen. Wie oben beschrieben werden auch Intermediary-Zertifikate häufiger aktualisiert, was man bei veralteten Java-Versionen manchmal erfährt, denn zu einer Java Runtime gehört auch ein Set an vertrauenswürdigen CAs.

Oben beschrieben wurde im Rahmen der Client-Authentifizierung, dass dem Server eine komplette Chain präsentiert wird. Diese Chain muss also in der Client-Applikation konfiguriert werden. Obwohl die in der Chain enthaltenen Zertifikate durch ihre Subject- und Issuer-DNs eine logische Reihenfolge bilden und somit sortierbar sind, muss sichergestellt werden, dass die Zertifikate in der richtigen Reihenfolge eingerichtet und als Liste an den Server geschickt werden. Der Server muss dann nicht mehr lange sortieren, sondern er prüft die Chain sequentiell von Anfang bis Ende.

An dieser Stelle wird der Umgang mit Zertifikaten leider etwas umbequem, denn üblicherweise liegen die passenden Dateien im PEM- oder DER-Format vor. Deren Encoding (Base64 oder binär) ist nur mit weiteren Hilfsmitteln lesbar. Dazu kommt auch noch, dass zwar der Client niemals seinen Private Key an einen anderen Server schicken sollte, doch wird auch mit dem Private Key stets gemeinsam mit den anderen Zertifikaten der Chain im Code umgegangen - immerhin ist er mit dem korrespondierenden Public Key Teil der Chain. Die Chain bzw. in Java der KeyStore ist also nicht nur unbequem, sondern auch noch mit Vorsicht zu behandeln.

Das folgende Code-Snippet zeigt ein Beispiel, wie in Java ein KeyStore aus Private Key und der passenden Chain erzeugt werden kann:

PrivateKey privateKey = ...
Certificate[] certs = ...

KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType())
keyStore.load((KeyStore.LoadStoreParameter) null)

keyStore.setKeyEntry(alias, privateKey, keyStorePwd.chars, certs)

Letztlich können auch das im JDK enthaltene keytool oder GUIs wie Portecle an der Komplexität nicht viel ändern. Dieser Artikel sollte aber zumindest Anhaltspunkte geben, wie das Puzzle vernünftig zusammengesetzt werden kann, wenn die einzeln Puzzleteile als Zertifikate vorliegen. Mit dem passenden Verständnis fällt dann auch die Konfiguration beispielsweise eines HAProxys leichter.

Im Kern lautet die Aussage zum Aufbau einer validen Chain: beginne bei dem Private Key und hangele dich entlang der Issuer bis zum Ende der Chain. Die dabei gefundenen Zertifikate bilden die Chain und können oft in einer einzigen Datei (wie gleich im Beispiel gezeigt) gespeichert werden.

Beispiel-Chain

Das Beispiel von oben (openssl s_client -showcerts -connect example.com:443) zeigt nebenbei schon viele Details einer Zertifikats-Chain, unter anderem die Zertifikate im PEM-Format. Der Private Key des Servers fehlt dort natürlich (hoffentlich).

Ein Private Key ist glücklicherweise im PEM-Format anhand der Pre-/Postfixe gut von den Zertifikaten unterscheidbar:

-----BEGIN RSA PRIVATE KEY-----
MII1...
-----END RSA PRIVATE KEY-----

Eine vollständige Chain würde ganz unspektakulär wie folgt aussehen:

-----BEGIN CERTIFICATE-----
MII2...                       <-- enthält den Public Key des
                                  oben aufgeführen Private Keys
-----END CERTIFICATE-----
-----BEGIN CERTIFICATE-----
MII3...                       <-- Zertifikat des Issuers
                                  zu obigem Public Key
-----END CERTIFICATE-----
-----BEGIN CERTIFICATE-----
MII4...                       <-- Zertifikat der Root CA
-----END CERTIFICATE-----

Wesentlich ist hier, dass der Private Key zum Zertifikat MII2... gehört. Das Zertifikat MII3... des Issuers von MII2... folgt direkt darauf. Dann folgt ein weiteres Zertifikat MII4..., das den Issuer für MII3... repräsentiert. Dieses Zertifikat gehört zu einer Root CA, die keinen weiteren übergeordneten Issuer hat, sondern sich das Zertifikat selbst signiert hat. Am Ende vertrauen wir also meist nur darauf, dass wir durch die Betriebssystemhersteller, Java-Updates oder Browser-Updates die stets gültigen und vertrauenswürdigen Zertifikate der Root-CAs erhalten. Wer diesen Automatismen nicht traut, kann sich von der CA seines Vertrauens die Zertifikate selbst runterladen und lokal an den diversen Stellen einrichten.

ASN.1 - Was?

Ein weiteres Thema in diesem Kontext sind auch die Algorithmen, mit denen Keys signiert werden. RSA und ECDSA gehören dabei noch zu den bekannteren Varianten. Welche Parameter man zu verwenden hat, sieht man leider nicht ohne Weiteres dem Key an. Für neuere OpenSSL Versionen wird sogar nur noch BEGIN PRIVATE KEY (PKCS8-Format) anstelle von BEGIN RSA PRIVATE KEY (PKCS1-Format) ausgegeben. Tatsächlich trägt der Key dann einen sogenannten Object Identifier (OID) im ASN.1 Encoding Format. Mit Hilfe dieses ASN.1 OID kann der passende Algorithmus zum Lesen bzw. Verfifizierens eines Keys ausgewählt werden.

Das klingt alles überraschend kompliziert, lässt sich aber leicht an Libraries delegieren: Die bekannte Library BouncyCastle unterstützt in ihrem JcaPEMKeyConverter die Varianten für ECDSA, DSA und RSA. Falls das nicht reichen sollte, können unter http://www.oid-info.com/ Details zu einer OID nachgeschlagen werden - mit den Informationen kann dann eine passende Implementierung gefunden werden. Für SHA512withRSA lautet die ASN.1 OID beispielsweise 1.2.840.113549.1.1.13.

Fazit

Zertifikatsverwaltung, (Mutual) TLS und Client-Authentifizierung sind kein Hexenwerk und mit den geeigneten Tools lassen sich einige Fallstricke vermeiden. Wichtig ist jedoch, dass man mit einigen Grundkonzepten vertraut ist und sich von den technischen Details nicht verwirren lässt. Falls man doch mal was falsch konfiguriert hat, oder Zertifikate innerhalb der Chain vertauscht sind, merkt man das wohl spätestens beim ersten fehlgeschlagenen Verbindungsaufbau ;)

Dieser Artikel versucht einige praxisrelevante Hinweise zu geben, ohne allzu tief ins TLS-Protokoll einzusteigen. Sollten einige Formulierungen unklar oder gar falsch sein, dann freuen wir uns über Hinweise unter @gesellix oder @EuropaceTech!