TLS Authentifizierung ohne Trust Infrastruktur

vorhergehende Artikel in: PKI-X.509-CA Linux Security
01.05.2025

Ich will hier natürlich nicht Public Key Infrastructures und Certificate Authorities abschaffen - Ich werde einfach einen wohldefinierten Anwendungsfall schildern, der es erlaubt, TLS-Kommunikation ohne diese Infrastruktur - und sogar mit flüchtigen Schlüsseln und Zertifikaten! - sicher zu betreiben. zu könne

Eins noch, bevor es los geht...

Ich rede hier immer von mTLS - also dem Modus, in dem sich sowohl der Server als auch der Client sich dem jeweils anderen gegenüber mittels seiner Digitalen Identität ausweist.

Traditionelles TLS

Bei traditionellem TLS, wie sie etwa der Kommunikation per HTTPS zum Einsatz kommt, unterhalten sich ein Server und mehrere Clients: Die Clients initiieren die Verbindung. Server antworten darauf mit ihrer Digitalen Identität und einer Liste von Herausgeber-Zertifikaten.

Der Client prüft nun die Gültigkeit und Vertrauenswürdigkeit des serverzertifikats. Anschließend dient dann diese Liste dem Client dazu, eine seiner Digitalen Identitäten auszuwählen, die er dem Server vorweist, um den Verbindungsaufbau abzuschließen: Es wird immer eine ausgewählt, deren zugehöriges Zertifikat in seiner Kette eines der vom Server empfangenen Herausgeber-Zertifikate hat.

Der Server prüft nun ebenfalls Gültigkeit und Vertrauenswürdigkeit anhand des Client-Zertifikats. Anschließend wird der Server anhand des Fingerprint der Client-Identität entscheiden, welche Rechte der Client erhält.

Man sieht hieran, dass dafür sehr viel Infrastruktur vorgehalten werden muss: Auf Server- wie auf Client-Seite muss ein Truststore (Eine Liste von Zertifikaten vertrauenswürdiger Aussteller) gepflegt werden, um die Validierung der Vertrauenswürdigkeit durchführen zu können. Ebenfalls auf beiden Seiten wird ein Keystore benötigt, der die jeweiligen Digitalen Identitäten speichern kann. TrustStores müssen unbedingt sicher vor Modifikationen geschützt werden, Keystores ebenso unbedingt vor unberechtigtem Auslesen des Inhaltes.

Dazu kommt noch eine entsprechende Datenbank auf der Serverseite für das Enrollment der Client-Identitäten, um darin Informationen über die Rollen und Rechte des hjeweiligen Client anhand der Fingerprints der jeweiligen Zertifikate zu speichern. Diese Datenbank muss natürlich ebenfalls administriert und gepflegt werden.

Motivation

Das Vertrauen und die gegenseitige Authentifizierung muss in einem solchen Fall natürlich anders etabliert werden, da es ja keine zentralen Trust-Anchor mehr geben kann. Wie lässt sich aber so etwas realisieren?

Beschreibung des Use Case

Ich beschreibe hier die Benutzung von TLS zur Absicherung einer zeitlich begrenzten Kommunikation zwischen zwei Parteien, denen ein unabhängiger, sicherer Kanal zur Übermittlung der Informationen zur Verfügung steht, die zur gegenseitigen Authentifizierung benötigt werden.

Ein wenig weniger abstrakt bedeutet das, dass ich hier eine Möglichkeit vorstelle, auf ähnliche Weise TLS-Kommunikation zu etablieren, wie man es von manchen Messengern kennt: Dort wird ein Bild auf dem HandyDisplay angezeigt und gefragt, ob dies dem auf dem jeweils anderen Endgerät angezeigten gleicht - Falls ja, wurde die Authentizität des jeweils anderen Kommunikationspartners verifiziert und die Datenübertragung kann beginnen.

Ein anderes Analogon dafür ist das Pairing zwischen Bluetooth-Geräten oder der Abgleich des Fingerabdrucks bei der ersten Verbindungsaufnahme mit einem TLS-Server (wenn auch in diesem Beispiel nur einseitig).

Genau dieses Schema ist auch mit TLS möglich - ich habe eine Beispielimplementierung dafür geschaffen.

Voraussetzung ist hier natürlich, dass - wie im Messager-Beispiel - beide Applikationen, die jeweils die Rollen des TLS-Servers und -Clients innehaben interaktive Anwendungen sind, die zumindest während der Etablierung der sicheren Kommunikation von Anwendern bedient werden, die sich - entweder direkt oder indirekt - unterhalten können.

Das Verfahren

Das Verfahren ist recht einfach: Man muss dazu lediglich auf beiden Seiten einen TrustManager implementieren, der die Entscheidung, ob dem jeweiligen Kommunikationspartner vertraut wird mittels einer Rückfrage beim Anwender trifft.

Ich habe dazu den Fingerprint des PublicKey des Komunikationspartners zunächst mittels einer einfahcen XOR-Operation auf 8 Byte reduziert. Anschließend habe ich die daraus resultierenden Daten mittels des in RFC1751 Verfahrens in einfache Worte kodiert.

Der Anwender wird nun aufgefordert, zu bestätigen, dass diese Worte denen entsprechen, die der jeweilige Kommunikationspartner in seiner GUI als Repräsentation des Fingerprints seines eigenen PublicKey angezeigt wird.

Damit auch wirklich eine Authentifizierung erfolgt und nicht einfach nur unhinterfragtes Durchklicken wie zum Beispiel bei der SSH-Meldung, dass sich ein Hostkey geändert hat stattfindet, habe ich noch ein wenig variiert: Ich füge zu den 6 Worten aus der RFC1751-Konvertierung ein weiteres hinzu und ändere die Reihenfolge zufällig - Der Anwender ist nun gezwungen, sich mit der zum Vergleich auf der Gegenseite erzeugten Repräsentation auseinanderzusetzen, denn er muss die 6 korrekten Worte auswählen und sie in der geforderten Reihenfolge aneinanderreihen.

So würde ein Server beispielsweise folgende Repräsentation seiner ID darstellen:

HAND SINK LEER HATH WAVE GANG

Und der Anwender am Client würde die Aufgabe haben, aus dieser Folge von Wörtern die sechs Richtigen in der korrekten Reihenfolge anzuordnen:

SINK LEER WAVE HAND WART GANG HATH

Diese Aufgabe kann sogar mit einem simplen numerischen KeyPad erledigt werden - dann würde der Anwender die 6 Worte nicht eingeben, sondern lediglich unter jedes der Worte seine Position - 0 stünde dann für das Wort, das nicht dazugehört. Für das oben bereits gegebene Beispiel würde die korrekte Lösung dann wie folgt aussehen:

SINK LEER WAVE HAND WART GANG HATH
  2    3    5    1    0    6    4

Der Code des TrustManager, den ich dafür geschrieben habe ist hier zu sehen:

import javax.net.ssl.*;
import java.io.*;
import java.security.*;
import java.security.cert.CRLException;
import java.security.cert.CertPathValidatorException;
import java.security.cert.CertPathValidatorResult;
import java.security.cert.CertificateException;
import java.security.cert.CertificateFactory;
import java.security.cert.X509CRL;
import java.security.cert.X509Certificate;

public abstract class EphemeralTrustManager extends java.lang.Object implements X509TrustManager { private final static org.slf4j.Logger CLASS_LOGGER = org.slf4j.LoggerFactory.getLogger(EphemeralTrustManager.class); private final static org.slf4j.Logger EXCEPTION_LOGGER = org.slf4j.LoggerFactory.getLogger("ExceptionCatcher");

private boolean doPKIXPathValidation; private java.util.Map<java.lang.String, X509Certificate> trustedCerts; private UserDecision userDecision;

public EphemeralTrustManager(UserDecision userDecision,boolean doPKIXPathValidation) { super(); if(userDecision==null) throw new IllegalArgumentException("userDecision must not be null!"); this.userDecision=userDecision; this.doPKIXPathValidation = doPKIXPathValidation; trustedCerts = new java.util.HashMap(); }

public KeyStore getKeyStore() throws KeyStoreException, NoSuchAlgorithmException, CertificateException, IOException { KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType()); keyStore.load(null, null); CLASS_LOGGER.debug("trustedcerts size: " + trustedCerts.size()); for (java.lang.String s : trustedCerts.keySet()) { keyStore.setCertificateEntry(s, trustedCerts.get(s)); } return keyStore; }

public void checkTrusted(X509Certificate[] chain, String authType) throws CertificateException { try { String fingerprint = de.elbosso.util.security.Utilities.calculateFingerprint(chain[0].getPublicKey(), "SHA256"); if (acceptCertificate(chain, authType)) { if (trustedCerts.containsKey(fingerprint) == false) trustedCerts.put(fingerprint, chain[0]); } else throw new java.security.cert.CertificateException("key matching failed!"); } catch (Throwable t) { EXCEPTION_LOGGER.warn(t.getMessage(),t); throw new CertificateException(t); } }

protected boolean acceptCertificate(X509Certificate[] chain, java.lang.String authType) { boolean rv = false; try { String fingerprint = de.elbosso.util.security.Utilities.calculateFingerprint(chain[0].getPublicKey(), "SHA256"); validate(fingerprint, chain[0]); rv = userDecision.acceptCertificate(chain, authType); } catch (java.lang.Throwable t) { EXCEPTION_LOGGER.warn(t.getMessage(),t); } return rv; }

private void validate(java.lang.String fingerprint, X509Certificate cert) throws KeyStoreException, NoSuchAlgorithmException, CertificateException, IOException, InvalidAlgorithmParameterException, CertPathValidatorException, CRLException { KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType()); keyStore.load(null, null); keyStore.setCertificateEntry(fingerprint, cert); CertificateFactory cf = CertificateFactory.getInstance("X.509");

java.util.Collection<X509CRL> crls = new java.util.LinkedList(); // CLASS_LOGGER.debug(keyStore.size()+" "+keyStore+" "+crls+" "+crls.size()); java.security.cert.PKIXParameters params = new java.security.cert.PKIXParameters(keyStore); if (crls != null) { java.security.cert.CertStoreParameters revoked = new java.security.cert.CollectionCertStoreParameters(crls); params.addCertStore(java.security.cert.CertStore.getInstance("Collection", revoked)); } params.setRevocationEnabled(((crls != null) && (crls.isEmpty() == false))); if (doPKIXPathValidation) { { int keyLength = de.elbosso.util.security.Utilities.getKeyLength(cert.getPublicKey()); if ((keyLength > -1) && (keyLength < 2048)) throw new java.lang.IllegalArgumentException("certificate key too weak"); }

java.security.cert.CertPath certpath = cf.generateCertPath(java.util.Arrays.asList(new X509Certificate[] {cert})); // CLASS_LOGGER.debug(certpath.toString()); java.security.cert.CertPathValidator validator = java.security.cert.CertPathValidator.getInstance("PKIX"); CertPathValidatorResult certPathValidatorResult = validator.validate(certpath, params); CLASS_LOGGER.trace(certPathValidatorResult.getClass().toString()); CLASS_LOGGER.trace(certPathValidatorResult.toString()); } } public interface UserDecision { boolean acceptCertificate(X509Certificate[] chain, java.lang.String authType); } }

Man kann sehen, dass die Art und Weise, wie der Anwender letztlich die Repräsentation des Fingerprint der Gegenstelle bestätigt in eine Implementierung des Interface UserDecision ausgelagert wurde - Die hier beschriebene Implementierung unter Verwendung der RFC1751 ist hier zu sehen:

import java.security.cert.X509Certificate;
import de.elbosso.util.security.Rfc1751;

public class DefaultRFC1751UserDecision extends java.lang.Object implements de.elbosso.util.security.EphemeralTrustManager.UserDecision { private final static org.slf4j.Logger CLASS_LOGGER = org.slf4j.LoggerFactory.getLogger(DefaultRFC1751UserDecision.class); private final static org.slf4j.Logger EXCEPTION_LOGGER = org.slf4j.LoggerFactory.getLogger("ExceptionCatcher");

public boolean acceptCertificate(X509Certificate[] chain, java.lang.String authType) { boolean rv = false; try { java.lang.String fingerprint = de.elbosso.util.security.Utilities.calculateRFC1751Fingerprint(chain[0].getPublicKey(), "SHA256"); // CLASS_LOGGER.debug(fingerprint); java.util.List<java.lang.String> l = new java.util.LinkedList(java.util.Arrays.asList(fingerprint.split(" "))); l.add(Rfc1751.randomSample()); java.util.Collections.shuffle(l); CLASS_LOGGER.debug("shuffled fingerprint: " + de.elbosso.util.Utilities.toString(l)); CLASS_LOGGER.debug("Enter characters:"); java.lang.StringBuffer buf = new java.lang.StringBuffer(); int input; while ((input = System.in.read()) != '\n') { buf.append((char) input); } CLASS_LOGGER.debug("read: " + buf.toString()); rv = fingerprint.equals(buf.toString()); } catch (

java.lang.Throwable t) { EXCEPTION_LOGGER.warn(t.getMessage(), t); } return rv; } }

Die ursprüngliche RFC1751 erwähnt bereits, dass man dieses Verfahren auch internationalisieren kann - dazu müsste man entsprechende Wörterbücher in den jeweiligen Zielsprachen erstellen. Das ist auch in einer Anwendung möglich, die das hier vorgestellte Schema der gegenseitigen Authentifizierung einsetzt. Allerdings muss man natürlich sicherstellen, dass immer beide Seiten dasselbe Wörterbuch benutzen.

Alternativ - und mit deutlich weniger Beschränkung der Allgemeingültigkeit - könnte man auch statt der Wörter selbst deren Stelle im Wörterbuch benutzen - das wären dann die Zahlen zwischen 0 und 2047 (bzw. zwischen 1 und 2048). Das wird aber schon wieder ein wenig nutzerunfreundlicher. Gleichzeitig sollte man jedoch auch Inklusion mitdenken - nicht jeder Mensch kann (aus verschiedenen Gründen) lesen. Sieht man sich in der Welt ein wenig um, dann sieht man dort aus ebenjenen Gründen: Piktogramme!

Daher die Idee, RFC1751 einfach mittels Piktogrammen umzusetzen - diese könnten aus dem Vorrat der Unicode-Zeichen genauso genommen werden, wie etwa aus den Material Design Icons. Das sind nur zwei Beispiele - es lassen sich sicher weitere Quellen erschließen, die mindestend 2048 unterschiedliche Symbole zur Verfügung stellen könnten.

Das oben angegebene Beispiel könnte etwa mit Unicode-Zeichen wie folgt dargestellt werden:

Screenshot Beispiel der Darstellung von 7 Symbolen mittels Symbolen aus Unicode-Zeichen

Unter Benutzung der Material Icons wäre wie beschrieben ebenfalls die Darstellung mittels Symbolen möglich:

Screenshot Beispiel der Darstellung von 7 Symbolen mittels einer Auswahl an Material Icons

Alle Artikel rss Wochenübersicht Monatsübersicht Codeberg Repositories Mastodon Über mich home xmpp


Vor 5 Jahren hier im Blog

  • Strange Attractors im Lorenz System

    10.05.2020

    Das Lorenz System kann in einem Jupyter-Notebook interaktiv erkundet werden:

    Weiterlesen...

Neueste Artikel

  • Anpassungen für GraphViz und PlantUML

    Ich habe hier schon verschiedentlich über die Anwendung des PlantUML- oder GraphViz- Formats geschrieben. Beide sind extrem vielseitig - sowohl für eher traditionelle Darstellungen von Graphen oder Entity-Relationship-Diagrammen als auch für zum Beispiel die Dokumentationen von Public Key Infrastrukturen

    Weiterlesen
  • Origami - Inspirationen und Anleitungen

    Videos und Bastelanleitungen - meistenteils Origami

    Weiterlesen
  • SBOMs für alte Java-Projekte

    Ich habe neulich wieder einmal über Software Bill Of Materials oder SBOMs nachgedacht - inspiriert nicht nur, aber auch von meinem $dayjob...

    Weiterlesen

Manche nennen es Blog, manche Web-Seite - ich schreibe hier hin und wieder über meine Erlebnisse, Rückschläge und Erleuchtungen bei meinen Hobbies.

Wer daran teilhaben und eventuell sogar davon profitieren möchte, muss damit leben, daß ich hin und wieder kleine Ausflüge in Bereiche mache, die nichts mit IT, Administration oder Softwareentwicklung zu tun haben.

Ich wünsche allen Lesern viel Spaß und hin und wieder einen kleinen AHA!-Effekt...

PS: Meine öffentlichen Codeberg-Repositories findet man hier.