TLS mit alternativem Vertrauensmanagement

19.02.2026

Wie bereits angekündigt werde ich in den nächsten Wochen einige Aspekte asymmetrischer Kryptographie beschreiben. Der vorliegende Artikel erläutert nochmals eine Alternative zum klassischen Vertrauensmanagement und demonstriert die Implementierung in Java.

Asymmetrische Kryptographie ist die Grundlage von Transport Layer Security (TLS). Das Protokoll arbeitet normalerweise mit elektronischen Zertifikaten, die immer die Authentizität des Servers und oft die des Clients (bei mutual TLS oder mTLS) bestätigen.

Damit dies funktioniert müssen auf beiden Seiten die (un-)mittelbaren Herausgeber der jeweiligen Zertifikate als vertrauenswürdig eingestuft sein - irgendein CA-Zertifikat in der Zertifikatskette muss in einem Truststore liegen, der dem jeweiligen Anwendungsfall zugeordnet ist.

Diese Herangehensweise ist der Natur des Standardanwendungsfalls für TLS geschuldet: Die Client-Anwendung (zum Beispiel ein Webbrowser) ist nicht in der Lage, vorherzusehen, mit welchem Web-Server sie sich verbinden wird - daher kann sie keine Liste mit vertrauenswürdigen Servern vorhalten, sondern muss sich damit zufriedengeben, ob das Zertifikat des jeweiligen Servers von einer vertrauenswürdigen Authority ausgestellt wurde.

Gefährlich aus Cybersecurity-Sicht ist hierbei ein selbstsigniertes Zertifikat, das statt eines von einer CA herausgegebenen benutzt wird: Es lässt sich nicht gegen die Liste der Trust-Anker vergleichen und hat keinerlei Aussagekraft über die Authentizität des Kommunikationspartners, der ein solches vorweist.

Es gibt aber auch andere Anwendungsfälle: Einer wäre zum Beispiel eine Kommunikation, bei der beide Seiten genau wissen, mit welchem Kommunikationspartner sie interagieren werden oder dies mit wenig Aufwand verifizieren können.

Ein Beispiel für dieses Szenario ist, dass zwei Geräte nur miteinander kommunizieren sollen, man jedoch trotzdem die Vertraulichkeit und Integrität der Kommunikation schützen möchte. Besteht neben dem per TLS abzusichernden Kommunikationskanal noch ein zweiter sicherer, so kann man sich des Protokolls TLS bedienen ohne eine Public Key Infrastructure aufbauen oder Trust Management betreiben zu müssen. Ganz nebenbei wird in diesem Szenario die Gefahr durch selbstsignierte Zertifikate beseitigt - hier lassen sich solche Zertifikate ohne Verminderung der Cybersecurity einsetzen.

Die Idee dabei ist, das Protokoll selbst ein wenig zu ändern und die Entscheidung der Vertrauenswürdigkeit des Kommunikationspartners davon abhängig zu machen, ob der Fingerprint des für das TLS-Protokoll benutzten Schlüssels mit dem übereinstimmt, der über den unabhängigen, sicheren Kanal kommuniziert wurde. Dazu wird eine Challenge angezeigt, die aus dem Schlüssel abgeleitet wurde und nur mit den über den sicheren Kanal gewonnenen Informationen korrekt beantwortet werden kann. Im Beispiel der beiden Geräte könnte das zum Beispiel dadurch geschehen, dass auf dem Bildschirm des einen Geräts eine Folge von Symbolen - eventuell aus RFC1751 - angezeigt wird, die aus seinem Schlüssel heraus generiert wurden und diese auf dem Kommunikationspartner eingegeben werden müssen, um sie mit der aus dem Schlüssel errechneten Symbolfolge zu vergleichen.

Dieses Vorgehen ähnelt sehr der in Messaging-Clients genutzten Methode, die Authentizität des Kommunikationspartners sicherzustellen.

Wie kann man dies nun im TLS-Protokoll verankern? Ich benutze hier wieder für die in der Sprache Java implementierten Beispiele Teile meiner diversen

online verfügbaren

Bibliotheken, die wiederum auf der Implementierung des

BouncyCastle-Projekts

beruhen.

Zunächst zeige ich den Code des Serverteils - er implementiert eine einfache Echo-Funktionalität und sendet empfangene Nachrichten an den jeweiligen Client zurück. Der Server selbst wird mit der folgenden Methode gestartet:

static void startServer(int port) throws Exception

char[] passphrase="123456".toCharArray();

KeyStore trustStore= de.elbosso.util.security.Utilities.createKeystoreWithJDKsTrustedRoots(); KeyStore digIdent= de.elbosso.util.security.Utilities.createSelfSignedDigitalIdenity("server",passphrase,2096,"RSA","CN=unknown",14,"SHA256withRSA");

ServerTrustManager trustManager=new ServerTrustManager(new DefaultRFC1751UserDecision(), true); SSLServerSocketFactory factory = de.elbosso.util.security.Utilities.createSslServerSocketFactory(digIdent,trustManager, passphrase); try (SSLServerSocket listener = (SSLServerSocket) factory.createServerSocket(port)) { boolean clientAuth=true; listener.setNeedClientAuth(clientAuth); CLASS_LOGGER.debug("listening for messages..."); de.netsysit.util.threads.ThreadManager tm=new de.netsysit.util.threads.ThreadManager("TLS-Chat",-1); while(true) { try { String fingerprint=de.elbosso.util.security.Utilities.calculateRFC1751Fingerprint(digIdent.getCertificate("server").getPublicKey(),"SHA256"); CLASS_LOGGER.debug("My own Fingerprint: "+fingerprint);

Socket socket = listener.accept(); if(SSLSocket.class.isAssignableFrom(socket.getClass())) { javax.net.ssl.SSLSocket sslSocket = (javax.net.ssl.SSLSocket) socket; } tm.execute(new ServiceThread(socket)); } catch(java.lang.Throwable t) { t.printStackTrace(); } } }

Der wichtige Teil hier ist der ServerTrustManager, eine von EphemeralTrustmanager abgeleitete Klasse, der eine Implementierung von UserDecision als Konstruktorparameter erhält:

public abstract class EphemeralTrustManager extends java.lang.Object implements X509TrustManager
{
    private final static org.slf4j.Logger CLASS_LOGGER = org.slf4j.LoggerFactory.getLogger(java.lang.invoke.MethodHandles.lookup().lookupClass());
    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); } }

Die hier genutzte Implementierung der UserDecision sieht wie folgt aus:

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(java.lang.invoke.MethodHandles.lookup().lookupClass());
    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 [] dictionary=de.elbosso.util.security.Rfc1751GlyphDictionary.getDictionary(); java.lang.String fingerprint = de.elbosso.util.security.Utilities.calculateRFC1751Fingerprint(chain[0].getPublicKey(), "SHA256", dictionary); // CLASS_LOGGER.debug(fingerprint); java.util.List<java.lang.String> l = new java.util.LinkedList(java.util.Arrays.asList(fingerprint.split(" "))); l.add(dictionary==null?Rfc1751.randomSample():dictionary[(int)(java.lang.Math.random()*(double)dictionary.length)]); 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; } }

Im Client kommen dieselben Mechanismen zum Tragen - die Methode zum Start des Client bedient sich ebenfalls eines custom TrustManagers, der wiederum eine UserDecision als Callback reingereicht bekommt:

public static void startClient(String host,int port) throws Exception
{
    char[] passphrase= "123456".toCharArray();

KeyStore digIdent= de.elbosso.util.security.Utilities.createSelfSignedDigitalIdenity("client",passphrase,2096,"RSA","CN=client",14,"SHA256withRSA");

String fingerprint=de.elbosso.util.security.Utilities.calculateRFC1751Fingerprint(digIdent.getCertificate("client").getPublicKey(),"SHA256"); CLASS_LOGGER.debug("My own Fingerprint: "+fingerprint);

KeyManagerWrapperFactory<X509KeyManager> kmwf=new KeyManagerWrapperFactory<X509KeyManager>(X509KeyManager.class) { @Override public KeyManager wrap(X509KeyManager client) { return new X509KeyManagerWrapper(client); } };

ClientTrustManager trustManager=new ClientTrustManager(new DefaultRFC1751UserDecision(), true); SSLSocketFactory factory = de.elbosso.util.security.Utilities.createSslSocketFactory(digIdent,trustManager,passphrase,kmwf); HostnameVerifier hostnameVerifier=new HostnameVerifier() { @Override public boolean verify(String hostname, SSLSession session) { try { CLASS_LOGGER.debug(java.lang.Integer.toString(session.getPeerCertificates().length)); String fingerprint=de.elbosso.util.security.Utilities.calculateFingerprint(session.getPeerCertificates()[0].getPublicKey(),"SHA256"); CLASS_LOGGER.debug(fingerprint); } catch(Throwable t) { t.printStackTrace(); } CLASS_LOGGER.debug("#ä return true; } }; try (SSLSocket socket = (SSLSocket) factory.createSocket(host, port)) { if (hostnameVerifier != null && !hostnameVerifier.verify(socket.getInetAddress().getHostName(), socket.getSession())) { throw new RuntimeException("\"" + socket.getInetAddress().getHostName() + "\" identity was not confirmed"); } CertificateFactory cf = CertificateFactory.getInstance("X.509");

java.security.KeyStore keystore=java.security.KeyStore.getInstance(java.security.KeyStore.getDefaultType()); keystore.load(null); String message = "Hello World Message"; OutputStream os = new BufferedOutputStream(socket.getOutputStream()); os.write(message.getBytes()); os.flush();

java.io.InputStream is = new BufferedInputStream(socket.getInputStream()); byte[] data = new byte[2048]; int len = is.read(data); System.out.printf("client received %d bytes: %s%n", len, new String(data, 0, len)); } }

Artikel, die hierher verlinken

Asymmetrische Kryptographie

28.03.2026

Ich habe mich mit der Idee schon länger getragen: Nochmal einen Rundumschlag zu asymmetrischer Kryptographie zu machen. Dabei werde ich mich auf Demonstrationen der einzelnen Konzepte und Operationen mit Beispielcode konzentrieren und zu jedem der vorgestellten Konzepte mehr oder weniger ausführlich bezüglich der Einsatzszenarien und Vor- und Nachteile Stellung beziehen

Anzeigen (und Öffnen) der zuletzt modifizierten Dateien im aktuellen Verzeichnis mit (Neo)Vim

19.02.2026

Ich habe bereits in früheren Artikeln beschrieben, wie ich mich stückweise von diversen Plugins für (Neo)Vim entwöhnt habe. Dieses Mal habe ich nicht etwa ein Plugin ersetzt, sondern mir in einem der benutzten Plugins fehlende Funktionalität erkämpft...

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


Vor 5 Jahren hier im Blog

  • xBrowserSync in Docker

    29.03.2021

    Nachdem ich schon längere Zeit nicht mehr über neue Dienste in meinem Docker-Zoo berichtet habe, habe ich in der vergangenen Woche wieder einmal einen Neuzugang begrüßen dürfen...

    Weiterlesen

Neueste Artikel

  • Asymmetrische Kryptographie

    Ich habe mich mit der Idee schon länger getragen: Nochmal einen Rundumschlag zu asymmetrischer Kryptographie zu machen. Dabei werde ich mich auf Demonstrationen der einzelnen Konzepte und Operationen mit Beispielcode konzentrieren und zu jedem der vorgestellten Konzepte mehr oder weniger ausführlich bezüglich der Einsatzszenarien und Vor- und Nachteile Stellung beziehen

    Weiterlesen
  • Windows? Nur noch gegen Bezahlung!

    Ich habe mich nun völlig von Windows - der armseligen Ausrede für ein Computerbetriebssystem aus Redmond - abgenabelt

    Weiterlesen
  • Vergleich Analoger und Digitaler Identitäten

    Eine Präsentation zum besseren Verständnis der Konzepte hinter digitalen Identitäten

    Aktualisierung vom 16. März 2025

    Aktualisierung der Präsentation mit einem Beispiel aus einem Film der 1980er Jahre und Betonung des Fakts, dass das Subjekt überhaupt nicht bemerken muss, dass eine Identität erstellt wird...

    Aktualisierung vom 17. August 2025

    Ein weiteres Beispiel wurde hinzugefügt.

    Aktualisierung vom 30. März 2026

    Aktualisierung der Präsentation: Erläuterung der Möglichkeit, mehr als ein Zertifikat für dasselbe Schlüsselpaar auszustellen und Exkurs zu Transport Layer Security als Beispiel der Forderung des Vorweisens bestimmter Arten digitaler Identitäten.
    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.