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
Bibliotheken, die wiederum auf der Implementierung des
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));
}
}
19.02.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
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...
Papers Februar 2021
25.02.2021
Auch wenn der Februar noch nicht ganz um ist haben sich bereits wieder - wie schon im Januar - vier Papers angefunden die ich hier kurz vorstellen möchte.
WeiterlesenAndroid Basteln C und C++ Chaos Datenbanken Docker dWb+ ESP Wifi Garten Geo Go GUI Gui Hardware Hardware. Links Java Java. Komponenten Jupyter JupyterBinder Komponenten Links Linuc Linux Markdown Markup Music Numerik OpenSource PKI-X.509-CA Präsentationen Python QBrowser Rants Raspi Revisited Security Software-Test sQLshell TeleGrafana Verschiedenes Video Virtualisierung Windows Upcoming...
Asymmetrische KryptographieIch 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
WeiterlesenIch 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...
WeiterlesenNach der letzten losen Zusammenstellung (für mich) interessanter Links aus den Tiefen des Internet von 2025 folgt hier gleich die erste für dieses Jahr:
WeiterlesenManche 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.