TLS-Kommunikation in Software

19.12.2023

Ich wollte bereits seit einiger Zeit ein Beispiel veröffentlichen für die Kommunikation über mit Transport Layer Security (TLS) gesicherte Kanäle. Nun habe ich endlich die Zeit gefunden, dies in Angriff zu nehmen:

Zunächst zur Abstreckung des Szenarios: Es sollte darum gehen, eine Verbindung zwischen Client und Server zu etablieren, die zur gegenseitigen Authentifizierung dem anderen ihre jeweilige digitale Identität vorweisen. Ich wollte hier also einen Schritt weiter gehen als das HTTPS-Protokoll, das zwar auch auf TLS basiert - jedoch weist sich hier nur der Server gegenüber den Clients aus, die Clients müssen zum Aufbau einer verschlüsselten Verbindung keine eigene digitale Identität übermitteln.

Zu einer digitalen Identität gehört immer ein öffentlicher und ein privater Schlüssel sowie ein elektronisches Zertifikat, das die Zugehörigkeit der Schlüssel zu den im Zertifikat enthaltenen Daten (Canonical Name, DNS-NAme,...) beglaubigt.

Diese drei Informationen könenn auf verschiedene Weise kodiert sein: die Schlüssel und Zertifikate können als eigenständige Entitäten vorliegen - es ist aber auch möglich, dass alles in einer Struktur zusammengefasst ist, die dem PKCS#12-Standard entspricht.

In dem von mir beschriebenen Szenario ist es so, dass beide - Client und Server - über eine solche digitale Identität verfügen müssen. Darüber hinaus ist es so, dass beide dem Zertifikat - bzw. dessen Aussteller - des jeweils anderen vertrauen müssen. Das kann gewährleistet werden, indem dieser Aussteller einer ist, dessen eigenem Zertifikat durch die im Betriebssystem festgelegten Regeln vertraut wird. ist das nicht der Fall, muss es möglich sein, Vertrauen zu definieren. Das bedeutet, dass es eine Möglichkeit geben muss, Client und Server eine Menge von Zertifikaten von solchen Ausstellern zur Verfügung zu stellen, die sie als vertrauenswürdig einstufen dürfen.

Das Schöne an wechselseitiger, auf Zertifikaten basierender Authentifizierung ist, dass auf der Seite des Servers anhand der Schlüssel bzw. Zertifikate eine Rechtevergabe stattfinden kann, da diese durch den Client nicht geändert werden können.

Ich habe die Demonstration zunächst mittels Java vorbereitet: Es existiert ein Server und ein Client die ein einfaches Echo-Protokoll vollziehen: Wann immer ein Client eine Botschaft an den Server sendet antwortet dieser mit einer entsprechenden Botschaft. Der Server sieht wie folgt aus:

static void startServer(int port) throws IOException, GeneralSecurityException
{
	java.io.File pemFile=new File("trusted-ca-certs.pem");
	java.io.File p12File=new java.io.File("serverkeystore.p12");
	char[] passphrase="password".toCharArray();

KeyStore trustStore= de.elbosso.util.security.Utilities.initializeTruststoreWithPemCertificates(pemFile,false); KeyStore digIdent= de.elbosso.util.security.Utilities.loadDigitalIdentityFromPKCS12(p12File,passphrase);

ServerSocketFactory factory = de.elbosso.util.security.Utilities.createSslServerSocketFactory(digIdent,trustStore, passphrase); try (SSLServerSocket listener = (SSLServerSocket) factory.createServerSocket(port)) { configurePort(listener); System.out.println("listening for messages..."); de.netsysit.util.threads.ThreadManager tm=new de.netsysit.util.threads.ThreadManager("TLS-Chat",-1); while(true) { try { Socket socket = listener.accept(); tm.execute(new ServiceThread(socket)); } catch(java.lang.Throwable t) { t.printStackTrace(); } } } }

Hier sieht man, wie die digitale Identität digIdent und die Menge vertrauenswürdiger Aussteller trustStore zur Erstellung einer ServerSocketFactory benutzt werden. Die genutzten Helper-Methoden sind in der Dependency

<dependency>
	<groupId>de.elbosso</groupId>
	<artifactId>util</artifactId>
	<version>2.1.0-SNAPSHOT</version>
</dependency>

im Repository

<repository>
	<id>repsy</id>
	<name>EL BOSSOs (https://elbosso.github.io/index.html) Maven Repository on Repsy</name>
	<url>https://repo.repsy.io/mvn/elbosso/default</url>
</repository>

für jeden verfügbar.

Im Quelltext sehen diese Methoden wie folgt aus:

public static java.security.KeyStore createKeystoreWithJDKsTrustedRoots() throws NoSuchAlgorithmException, KeyStoreException, CertificateException, IOException
{
	javax.net.ssl.TrustManagerFactory trustManagerFactory = javax.net.ssl.TrustManagerFactory.getInstance(javax.net.ssl.TrustManagerFactory.getDefaultAlgorithm());
	trustManagerFactory.init((java.security.KeyStore) null);

java.util.List<javax.net.ssl.TrustManager> trustManagers = java.util.Arrays.asList(trustManagerFactory.getTrustManagers()); java.util.List<java.security.cert.X509Certificate> certs = trustManagers.stream() .filter(javax.net.ssl.X509TrustManager.class::isInstance) .map(javax.net.ssl.X509TrustManager.class::cast) .map(trustManager -> java.util.Arrays.asList(trustManager.getAcceptedIssuers())) .flatMap(java.util.Collection::stream) .collect(java.util.stream.Collectors.toList());

// Create a KeyStore containing our trusted CAs java.security.KeyStore keystore = de.elbosso.util.security.Utilities.createKeystoreUseCnAsAlias(certs); return keystore; }

public static java.util.Collection<X509Certificate> loadCertificates(File certificateFile) throws IOException, CertificateException { try (FileInputStream inputStream = new FileInputStream(certificateFile)) { return (java.util.Collection<X509Certificate>) CertificateFactory.getInstance("X509").generateCertificates(inputStream); } }

public static void addDefaultTrustedCertificates(KeyStore trustStore) throws GeneralSecurityException, IOException { java.security.KeyStore trusted=de.elbosso.util.security.Utilities.createKeystoreWithJDKsTrustedRoots(); java.util.Enumeration<java.lang.String> en=trusted.aliases(); while(en.hasMoreElements()) { java.lang.String alias=en.nextElement(); java.security.cert.Certificate cert=trusted.getCertificate(alias); if((cert!=null)&&(X509Certificate.class.isAssignableFrom(cert.getClass()))) trustStore.setCertificateEntry(alias, (X509Certificate) cert); } }

public static KeyStore initializeTruststoreWithPemCertificates(File pemFile, boolean addDefaultRootCaCertificates) throws GeneralSecurityException, IOException { // Create a new trust store, use getDefaultType for .jks files or "pkcs12" for .p12 files KeyStore trustStore = KeyStore.getInstance(KeyStore.getDefaultType()); // Create a new trust store, use getDefaultType for .jks files or "pkcs12" for .p12 files trustStore.load(null, null);

// If you comment out the following, the request will fail java.util.Collection<X509Certificate> certificateCollection=loadCertificates(pemFile); for(X509Certificate x509Certificate:certificateCollection) { trustStore.setCertificateEntry( x509Certificate.getSubjectX500Principal().getName(X500Principal.RFC2253), x509Certificate ); } if(addDefaultRootCaCertificates) addDefaultTrustedCertificates(trustStore); return trustStore; }

public static KeyStore loadDigitalIdentityFromPKCS12(File p12File, char[] passphrase) throws IOException, CertificateException, NoSuchAlgorithmException, KeyStoreException { // Create a new trust store, use getDefaultType for .jks files or "pkcs12" for .p12 files KeyStore digIdent = KeyStore.getInstance("pkcs12"); FileInputStream fis=new FileInputStream(p12File); digIdent.load(fis, passphrase); fis.close(); return digIdent; }

public static SSLContext createSslContext(KeyStore digIdent, KeyStore trustStore, char[] passphraseForDigIdent) throws KeyManagementException, UnrecoverableKeyException, KeyStoreException, NoSuchAlgorithmException { TrustManagerFactory tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); tmf.init(trustStore); TrustManager[] trustManagers = tmf.getTrustManagers();

SSLContext sslContext = SSLContext.getInstance("SSL");

KeyManagerFactory kmf=KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm()); kmf.init(digIdent,passphraseForDigIdent); KeyManager[] keyManagers = kmf.getKeyManagers();

sslContext.init(keyManagers, trustManagers, null); return sslContext; }

public static SSLServerSocketFactory createSslServerSocketFactory(KeyStore digIdent, KeyStore trustStore, char[] passphraseForDigIdent) throws GeneralSecurityException { return createSslContext(digIdent,trustStore,passphraseForDigIdent).getServerSocketFactory(); } public static SSLSocketFactory createSslSocketFactory(KeyStore digIdent, KeyStore trustStore, char[] passphraseForDigIdent) throws GeneralSecurityException { return createSslContext(digIdent,trustStore,passphraseForDigIdent).getSocketFactory(); }

Der Server konfiguriert am Server-Socket nach seiner Erzeugung noch einige Aspekte - dazu gehören unter anderem die Festlegung,

  • dass er Verbindungen nur von Clients zulässt, die sich mittels einer digitalen Identität ausweisen können
  • die möglichen zu verwendenden Cipher-Suiten (hier stark gekürzt) und
  • die unterstützten TLS-Protokollversionen:

static void configurePort(SSLServerSocket listener)
{
	listener.setNeedClientAuth(true);
	listener.setEnabledCipherSuites(new String[] {
			"TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384",
			"TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256",
			//... https://www.java.com/en/configure_crypto.html
			"TLS_RSA_WITH_AES_128_CBC_SHA",
			"TLS_EMPTY_RENEGOTIATION_INFO_SCSV"
	});
	listener.setEnabledProtocols(new String[] { "TLSv1.3", "TLSv1.2" });
}

Der entsprechende Echo-Client in Java realisiert sieht dann wie folgt aus:

static void startClient(String host,int port) throws IOException, GeneralSecurityException
{
	java.io.File pemFile=new File("trusted-ca-certs.pem");
	java.io.File p12File=new java.io.File("clientkeystore.p12");
	char[] passphrase= "password".toCharArray();

KeyStore trustStore = de.elbosso.util.security.Utilities.initializeTruststoreWithPemCertificates(pemFile,false); KeyStore digIdent = de.elbosso.util.security.Utilities.loadDigitalIdentityFromPKCS12(p12File,passphrase);

SocketFactory factory = de.elbosso.util.security.Utilities.createSslSocketFactory(digIdent,trustStore,passphrase); try (SSLSocket socket = (SSLSocket) factory.createSocket(host, port)) { configurePort(socket); String message = "Hello World Message"; OutputStream os = new BufferedOutputStream(socket.getOutputStream()); os.write(message.getBytes()); os.flush();

InputStream is = new BufferedInputStream(socket.getInputStream()); byte[] data = new byte[2048]; int len = is.read(data); } }

Man erkennt auch hier wieder das generelle Vorgehen: Installieren einer digitalen Identität und eines Truststore im Kontext und Benutzung bei der Erzeugung neuer Socket-Verbindungen. Die Methode configurePort(socket) muss natürlich auf Seite des Client so ausgestaltet sein, dass sich Server und Client auf eine zu benutzende Cipher-Suite einigen können...

Und schließlich - auch um zu prüfen, dass ich hier nicht etwas gebaut habe, das aus irgendwelchen Gründen nur im Java-Ökosystem funktioniert - hier noch eine Implementierung des Client in Python (entsprechende weiterführende Informationen dazu findet man im Internet zum Beispiel hier und hier. Man erkennt die zentralen bereits oben besprochenen Punkte auch hier - die digitale Identität und die Trust-Anchors. Python bietet allerdings die Verarbeitung von PKCS#12-Dateien nicht an, daher wird hier das Zertifikat und der private Schlüssel als Teile der digitalen Identität separat übergeben:

import ssl
import socket
import pprint

def sslClient(): context = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT) context.load_verify_locations("trusted-ca-certs.pem") context.load_cert_chain("client-certificate.pem","clientprivkey.pem","password") conn = context.wrap_socket(socket.socket(socket.AF_INET),server_hostname="localhost") conn.connect(("localhost", 2222)) cert = conn.getpeercert() pprint.pprint(cert) conn.sendall(b"huhu, server!") result=conn.recv(1024) print(result) conn.shutdown(socket.SHUT_RDWR) conn.close()

if __name__ == '__main__': sslClient()

Aktualisierung vom 2. März 2023

natürlich haben aufmerksame Leser längst gemerkt, dass in den originalen Quelltextschnipseln zwwi grobe Schnitzer enthalten waren: nicht alle der Ciphersuiten unterstützen Perfect Forward Secrecy (PFS) und sollten daher eigentlich gar nicht mehr eingesetzt werden. Speziell betraf das

	"TLS_RSA_WITH_AES_128_CBC_SHA",
	"TLS_EMPTY_RENEGOTIATION_INFO_SCSV"

Statt dessen sollte in Java die Konfiguration besser wie folgt aussehen:

static void configurePort(SSLServerSocket listener)
{
	listener.setNeedClientAuth(true);
	listener.setEnabledCipherSuites(new String[] {
			"TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384",
			"TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256",
			"TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384",
			"TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256",
			"TLS_DHE_RSA_WITH_AES_256_GCM_SHA384",
			"TLS_DHE_DSS_WITH_AES_256_GCM_SHA384",
			"TLS_DHE_RSA_WITH_AES_128_GCM_SHA256",
			"TLS_DHE_DSS_WITH_AES_128_GCM_SHA256",
			"TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA384",
			"TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA384",
			"TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256",
			"TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256",
			"TLS_DHE_RSA_WITH_AES_256_CBC_SHA256",
			"TLS_DHE_DSS_WITH_AES_256_CBC_SHA256",
			"TLS_DHE_RSA_WITH_AES_128_CBC_SHA256",
			"TLS_DHE_DSS_WITH_AES_128_CBC_SHA256",
			"TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA",
			"TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA",
			"TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA",
			"TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA",
			"TLS_DHE_RSA_WITH_AES_256_CBC_SHA",
			"TLS_DHE_DSS_WITH_AES_256_CBC_SHA",
			"TLS_DHE_RSA_WITH_AES_128_CBC_SHA",
			"TLS_DHE_DSS_WITH_AES_128_CBC_SHA",
	});
	listener.setEnabledProtocols(new String[] { "TLSv1.3", "TLSv1.2" });
}

Aktualisierung vom 19. Dezember 2023

Ich habe Repsy verlassen - meine Maven-Artefakte sind nun hier

<repository>
	<id>gitlab</id>
	<name>EL BOSSOs (https://elbosso.github.io/index.html) Maven Repository</name>
	<url>https://elbosso.gitlab.io/mvn/repository/</url>
</repository>

zu finden. Für dieses Artefakt heißt das, dass auch alle RELEASE Versionen und die aktuellste SNAPSHOT Version demnächst dorthin umziehen werden:

<dependency>
	<groupId>de.elbosso</groupId>
	<artifactId>util</artifactId>
	<version>2.3.0-SNAPSHOT</version>
</dependency>

In der Übergangszeit kann es zu einigen Problemen kommen - falls das so ist, bitte ich um ein kurzes Feedback - zum Beispiel in Gestalt eines Issues an dem betreffenden Projekt.

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


Vor 5 Jahren hier im Blog

  • Multi-User-WebDAV, Docker, GitHub

    17.11.2019

    Nachdem ich mich in letzter Zeit verstärkt mit Docker und dem zugehörigen Ökosystem beschäftige, habe ich begonnen, verschiedenste Dienste in Containern zu testen um zu sehen, ob in manchen Fällen LXC oder KVM nicht doch die bessere Wahl wäre...

    Weiterlesen...

Neueste Artikel

  • Migration der Webseite und aller OpenSource Projekte

    In eigener Sache...

    Weiterlesen...
  • AutoHideToolbar für Java Swing

    Ich habe eine neue Java Swing Komponente erstellt: Es handelt sich um einen Wrapper für von JToolBar abgeleitete Klassen, die die Werkzeugleiste minimieren und sie nur dann einblenden, wenn der Mauszeiger über ihnen schwebt.

    Weiterlesen...
  • Integration von EBMap4D in die sQLshell

    Ich habe bereits in einem früheren Artikel über meine ersten Erfolge berichtet, der sQLshell auf Basis des bestehenden Codes aus dem Projekt EBMap4D eine bessere Integration für Geo-Daten zu spendieren und entsprechende Abfragen, bzw. deren Ergebnisse auf einer Kartenansicht zu visualisieren.

    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.