Vertrauenswürdige Zertifizierungsstellen und Softwareentwicklung: Mehr Chaos als gedacht

Posted by jonas on Thursday, October 21, 2021

Neulich ist ja nun ein altes Root-Zertifikat von Let’s Encrypt abgelaufen und sehr zur Überraschung Vieler hat das zu einigen mehr - lösbaren - Problemen geführt als erwartet, auch bei uns. Eine der Ursachen ist, dass sich Listen vertrauenswürdiger Zertifizierungsstellen an viel mehr Stellen finden als man so gemeinhin annehmen würde.

Hintergrund

Kurz und knapp vorweg: Eine Liste vertrauenswürdiger Zertifizierungsstellen legt fest, welchen Zertifikaten vertraut wird und welchen nicht. Und wenn das jetzt irgendwie nach einer doch ziemlich wichtigen Geschichte klingt, dann, weil das eben auch wirklich so ist. Eine Angreiferin kann sich zwar problemlos ein Zertifikat erstellen, das auf die Domain deiner Bank lautet; keine vertrauenswürdige Zertifizierungsstelle sollte ihr allerdings eine Signatur dafür geben. Ohne diese wird dein Browser dich beim Aufruf der Domain per HTTPS mit Warnmeldungen überschütten, dass das Zertifikat nicht vertrauenswürdig ist.

Bei Linux-Distributionen ist es üblich, dass sie eine Liste von vertrauenswürdigen Zertifizierungsstellen mitbringen, die dann von allen Applikationen benutzt wird. Bei unserem CentOS ist das beispielsweise das RPM ca-certificates. Darin befindet sich die /etc/pki/tls/cert.pem, in der die Public Keys aller Zertifizierungsstellen enthalten sind, die die Mozilla Foundation für vertrauenswürdig hält. Solange die Distribution Updates erhält, wird auch diese Liste regelmäßig aktualisiert, denn wie das Leben so ist: Es kommen Zertifizierungsstellen dazu und es fliegen auch wieder welche raus, zum Beispiel wenn sie kompromittiert worden sind oder es sonstwie Anlass zu Missbrauch gibt. So weit, so gut. Eine Menge Komponenten hängen davon direkt ab, allen voran natürlich OpenSSL und damit so ziemlich jedes Tool, das irgendwas mit TLS zu tun hat: curl, wget, lynx, links, und noch viele mehr.

Es ist insofern klar, dass eine solche Liste für alle Menschen wichtig ist, die mit TLS-verschlüsselten Verbindungen arbeiten wollen, also unter anderem natürlich auch mit HTTPS. Jeder Mensch braucht so eine Liste. Und glücklicherweise bringt ja eben jene Linux-Distribution auch eine solche Liste mit.

Auch Menschen, die Software entwickeln, benötigen solche Listen, insbesondere dann, wenn sie mit HTTPS-basierten APIs arbeiten, denn wenn ich zum Beispiel Zahlungsverkehr mit einem Zahlungsdienstleister abwickeln will, dann möchte ich natürlich auch sicher sein, dass ich wirklich mit jenem Zahlungsdienstleister interagiere und nicht mit jemandem, der sich nur als jener ausgibt. Auch hier braucht es natürlich eine Liste vertrauenswürdiger Zertifizierungsstellen.

Mögliche Optionen

Und hier fängt es dann an, schwierig zu werden. Denn die Softwaremodule, die verschlüsselte Verbindungen aufbauen wollen, insbesondere Scriptsprachen, sind oft für verschiedene Betriebssysteme verfügbar, und jene Betriebsysteme haben unterschiedliche Mechanismen zur Verwaltung der Liste vertrauenswürdiger Zertifizierungsstellen, und selbst, wenn es sich dabei einfach nur um eine Textdatei handelt, dann muss diese nicht notwendigerweise bei den verschiedenen Distributionen an der gleichen Stelle liegen. Es bieten sich also verschiedene Ansätze an:

  • Das betreffende Softwaremodul könnte versuchen, je nach Betriebssystemplattform automatisch zu ermitteln, wo es “die” Liste vertrauenswürdiger Zertifizierungsstellen findet. Das wäre komfortabel; mit etwas Pech wird jene Liste aber nicht gefunden, oder es wird eine falsche gefunden.
  • Das betreffende Softwaremodul könnte die Anwenderin dazu auffordern, ausdrücklich anzugeben, welche Liste vertrauenswürdiger Zertifizierungsstellen benutzt werden soll. Das wäre sehr sauber, weil explizit; es würde aber voraussetzen, dass diese Information bekannt oder zumindest gut auffindbar ist.
  • Das betreffende Softwaremodul könnte eine entsprechende Liste einfach selbst mitbringen. Das wäre sehr komfortabel, bedeutet aber eben auch, dass es dann mehr als eine Liste auf dem System gibt, und dass damit auch mehr als eine Liste gepflegt werden muss.

Intuitiv erscheint mir die dritte Idee als die problematischste. Entsprechend groß war meine Überraschung, dass es aber offenbar auch die üblichste ist - mit allen erwartbaren Folgen: Auch wenn ich meine zentrale Liste vertrauenswürdiger Zertifizierungsstellen brav (z.B. mittels yum upgrade oder apt-get upgrade) auf dem aktuellsten Stand halten, heißt das eben noch lange nicht, dass meine eigenen, selbst geschrieben oder selbst installierten Applikationen jene dann auch benutzen.

Konkretes Beispiel

Wir hatten kürzlich schon einen konkreten Fall. Wir wollten nämlich unser bestehendes Setup von Request Tracker, den wir für den Support benutzen, auf eine neue Version und bei der Gelegenheit auch gleich auf eine neue Distribution bringen. Der Request Tracker hat eine wahre Hölle an Abhängigkeiten auf Perl-Module. Da wir in diesem Fall auf einem Debian 10 arbeiten, haben wir zunächst versucht, die betreffenden Module sauber via apt zu installieren, was auch für alle Module geklappt hat - bis auf eins: Mozilla::CA. Stellt sich also die Frage: Wieso ist noch das obskurste Perl-Modul ordentlich für Debian paketiert, dieses aber nicht? Stellt sich raus, es wurde 2011 und 2013 bereits angefragt - und abgelehnt. Das Debian-Team hielt es nämlich für eine ausgesprochen schlechte Idee, neben der Zertifikatsliste, die die Distribution bereits pflegt, noch eine weitere aufzunehmen. Jemand hat sich auch die Mühe gemacht, die Gründe, die dagegensprechen, ordentlich aufzuschreiben, darunter:

But when relying on Mozilla::CA, we rely on:

  • the maintainer to release often to keep Mozilla::CA in sync with the Mozilla certificates list
  • the maintainer of the module to be trustable (no compromised certificates introduced)
  • yourself or your sysadmin to keep your local copy of Mozilla::CA up to date with the latest CPAN release
  • the CPAN mirror from not being compromised to serve an altered version of Mozilla::CA
  • you can’t use additional root certificates installed on your system that are not in Mozilla list (unless the application allow to use multiple certificate databases)
  • you can’t filter the Mozilla list to exclude some certificates

Der springende Punkt ist: Als Anwenderin ist man sich dieses ganzen Themas vielleicht gar nicht bewusst. Man möchte gerne HTTPS mit irgendeiner Site sprechen, und folgt dabei irgendeinem Tutorial, das in etwa diesen Code vorschlägt:

use LWP::UserAgent;
my $ua = LWP::UserAgent->new;
my $response = $ua->get('https://irgendwas/');

Und das funktioniert dann auch einfach - und zwar inklusive korrekter Validierung des Hostnamens, weil die Option verify_hostname nämlich standardmäßig gesetzt ist. Es liegt insofern die Annahme nahe, dass die Validierung des Zertifikat gegen die Zertifikatsliste der Distribution durchgeführt wird.

Was LWP::Protocol::https aber tatsächlich an dieser Stelle tut, ist: Es versucht, wenn nicht explizit eine Datei mit einer Liste von Zertifizierungsstellen angegeben ist, das Modul Mozilla::CA zu laden, das eine eigene Kopie der Zertifizierungsstellen enthält. Normalerweise jedenfalls. Und nur wenn das fehlschlägt (weil zum Beispiel das Modul nicht installiert ist), weist es die Anwenderin an, eine anzugeben. Oder eben das Modul Mozilla::CA zu installieren - ein Rat mit Beigeschmack, wenn wir an die oben genannten Gründe denken.

Was Distributionen tun

Unterschiedliche Distributionen gehen unterschiedliche Wege. CentOS zum Beispiel paketiert ein hübsches RPM namens perl-Mozilla-CA, das dieses Modul enthält - aber patcht dessen Sourcecode so, damit es nicht die von ihm selbst mitgelieferte Liste von Zertifizierungsstellen benutzt, sondern die zentrale, die von CentOS gepflegt wird. Debian geht einen anderen Weg und stellt das Modul überhaupt nicht bereit; vermutlich, um deutlicher darauf hinzuweisen, für wie schlecht sie schon die Idee dieses Moduls halten. Dafür patchen sie aber ihr liblwp-protocol-https-perl-Paket, das normalerweise Mozilla::CA benutzt, dahingehend, dass das Modul die zentrale Liste der Distribution benutzt.

So oder so: Der obige Beispielcode funktioniert sowohl unter Debian als auch unter CentOS “einfach so” und benutzt dabei die Liste der Distribution, weil jene die betreffenden Module patcht. Danke dafür, allen beiden.

Wenn jemand nun aber z.B. mittels perlbrew sein eigenes Perl installiert oder auf Basis von local::lib oder Carton eigene Perl-Module installiert und da dann vielleicht Mozilla::CA dabei ist: Dann ist es eben anders und der Code funktioniert nur, solange die dort mitgelieferte Zertifikatsliste eben noch tut. Abhilfe schafft hier leider nur, explizit die korrekte Zertifikatsliste selbst anzugeben. Obiger Code sähe dann so aus:

use LWP::UserAgent;
my $ua = LWP::UserAgent->new(
  ssl_opts => {
    SSL_ca_file => '/etc/pki/tls/cert.pem',
  },
);
my $response = $ua->get('https://irgendwas/');

Es ist nun also nicht sonderlich schwierig, es korrekt zu machen - aber es ist unnötig schwierig. Es stellt einem unverhältnismäßig leicht ein Bein. Denn wie wir ja nun gerade live und schmerzlich gesehen haben: Die Liste vertrauenswürdiger Zertifizierungsstellen ändert sich konstant. Wie der Artikel Why Does Mozilla Maintain Our Own Root Certificate Store? so griffig schreibt:

Properly maintaining a root store is a significant undertaking – it requires constant effort to evaluate new trust anchors, monitor existing ones, and react to incidents that threaten our users.

Was für ein Chaos

Der Umstand, wieviele Projekte einigermaßen wild Kopien irgendeines Versionsstands der Liste irgendwo hinschreiben, wo sie möglicherweise nie mehr angefasst werden, sollte in diesem Kontext für die eine oder andere hochgezogene Augenbraue sorgen. Mozilla::CA ist hier nur ein Beispiel. Python hat mit certifi genau das gleiche in grün; Ruby ebenso; die zugrundeliegende Website certifi.io ist seit über einem Jahr down (was in mir jetzt nicht unbedingt allzu vertrauenswürdige Gefühle weckt), node.js kompiliert eine Liste von Zertifizierungsstellen direkt in sein Binary ein, und damit ist eben noch lange kein Ende. Die Idee, dass eben irgendwer eine von irgendwo bezogene Liste in irgendeinem Versionstand mitbringt und sie dann irgendwo im System installiert, wo sie dann möglicherweise nie wieder angefasst wird, scheint eben weit verbreitet.

All jene, die sich halt nun wissentlich oder unwissentlich (sprich, als automatisch aufgelöste Abhängigkeit) Mozilla::CA installiert haben, haben nun auch weiterhin eine nur für ihre eigene Applikation geltende Liste, die zu alt ist und weiterhin die abgelaufene “DST Root CA X3” beinhaltet. Mit anderen Worten, die eigene Applikation kann dann auf Distributionen mit OpenSSL 1.0.2 seit dem 30.09.2021 nicht mehr mit Let’s-Encrypt-Zertifikaten umgehen, obwohl sämtliche Distributionsupdates eingespielt sind.

Fazit

In meiner ersten Fassung für diesen Blogpost habe ich für diese Praxis noch Begriffe wie “bizarr” verwendet; inzwischen finde ich eigentlich eher, es ist eben eine schlechte Idee und die Alternativen sind auch schlecht. Wichtig ist insofern in erster Linie, sich der Problematik überhaupt bewusst zu sein - auf mich selbst traf das bis zum Ablauf des alten Root-Zertifikats in diesem Aspekt nämlich schlicht nicht zu.

Mein persönlicher Vorzug speziell bei sicherheitsrelevanten Themen wäre eigentlich: Weniger Gemauschel und mehr Explizitheit; wenn ich als Developer bei der Installation eines Softwaremodules aufgefordert würde, anzugeben, wo eine Liste vertrauenswürdiger Zertifizierungsstellen zu finden ist, wäre mir das allemal lieber, als dass ohne mein Wissen eine potentiell veraltete und damit auch potentiell dysfunktionale oder auch unsichere Liste auf meinem System landet. Es impliziert dann natürlich auch, dass ich grundlegendes Wissen über Zertifizierungsstellen brauche - aber gerade im sicherheitsrelevanten Bereich ist es dann vielleicht auch nicht zuviel verlangt, eben dies auch zu erwarten.

Photo by Yung Chang on Unsplash