BigBastis Blog

SSL Teil 4: Serverseitige Authentifizierung mit Java

Introduction

user

Sebastian Gross

Sebastian Gross arbeitet in Bielefeld als Softwareentwickler für .NET und Java im Bereich Web.Als Fan der .NET-Plattform lässt er sich kein Userguppen Treffen und Community Event im Raum OWL entgehen.Dabei hat er eine besondere Vorliebe für das ASP.NET MVC Framework und für das Test Driven Development (TDD) entwickelt.


LATEST POSTS

Handling too long scrollspy menus 10th June, 2015

Java: Create ZIP archive 23rd March, 2015

Allgemein

SSL Teil 4: Serverseitige Authentifizierung mit Java

Posted on .

Nachdem wir in den letzen Teilen mehr die Theorie durchgenommen haben, möchte ich jetzt zu dem praktischen Teil kommen und euch zeigen wie man diese Theorie in Code umsetzen kann am Beispiel Java. (.NET wird auch noch folgen)

Wir wollen einfach einsteigen und implementieren erst mal nur die eine Serverseitige Authentifizierung, also muss der Server sich uns gegenüber ausweisen und wir sind in der Pflicht sein Zertifikat anzunehmen oder abzulehnen.

Wie in Teil 1 erklärt benötigen wir also zunächst einen Ort wo wir die Zertifikate sichern, denen wir vertrauen. Hier können wir (da wir in Java unterwegs sind) nicht die Windows-eigenen nehmen sondern müssen die so genannten Keystores von Java Nutzen.

Ein Keystore, wie der Name schon sagt ist nichts anderes als ein Speicher für Zertifikate. Wenn ihr Java installiert dann legt Java bereits einen Keystore für euch an, diesen findet ihr dann in eurem Java Verzeichnis Java\jre6\lib\security\cacerts die Datei „cacerts“ enthält hier bereits ähnlich wie bei Windows bereits die CA-Zertifikate von den großen Zertifizierungsstellen.

Es wir desweiterhin in Java unterschieden zwischen  Keystore und Truststore, beides sind im Grunde Keystores und unterscheiden sich nur im Namen und in ihrem Inhalt. Eine einzige Datei kann auch beide beinhalten, ist aber eher unüblich, da man die eigenen (privaten) Zertifikate von den CA-Zertifikaten separieren will.

Zuständigkeiten

So enthalten die Keystores für gewöhnlich die eigenen Privaten Schlüssel und die eigenen von anderen CAs signierte Zertifikate, die bei Verbindungen als Serverzertifikate (Public Keys) genutzt werden. Der Truststore ist das Gegenstück und enthält die Zertifikate denen man vertraut. So müssen wir die Zertifikate die wir vom Server beim Verbindungsaufbau erhalten gegen einen Truststore prüfen.

Fangen wir mit unserem Code an, wir wollen einen Client erstellen, der eine Socket-basierte Verbindung zu einem Server über SSL aufbaut und das vom Server zurückgegebene Zertifikat prüft.

Java ist auf diesem Gebiet sehr entgegenkommend und bietet für fast alle Anwendungsfälle fertige APIs und Klassen die uns fast die ganze Arbeit abnehmen.

Um den Prozess besser aufzeigen zu können schreiben wir uns eine Eigene SSLSocketFactory die für uns den Aufbau der Verbindung übernimmt und unserem Programm im Prinzip ein fertiges Socket liefert.

Dazu erstellen wir eine neue Klasse und lassen diese von SSLSocketFactory erben. Da das eine abstrakte Klasse ist müssen eir einige Methoden implementieren. Wenn das erledigt ist sieht unsere Klasse schon so aus:

/**
 * Stellt eine SSLSocketFactory bereit die einen angegebenen TrustStore nutzt
 * @author Sebastian Gross
 */
public class ServerSSLAuthSocketFactory extends SSLSocketFactory{

    @Override
    public String[] getDefaultCipherSuites() {

    }

    @Override
    public String[] getSupportedCipherSuites() {

    }

    @Override
    public Socket createSocket(Socket socket, String string,
            int i, boolean bln) throws IOException {

    }

    @Override
    public Socket createSocket(String destinationAddress,
            int destinationPort) throws IOException, UnknownHostException {

    }

    @Override
    public Socket createSocket(String string, int i,
            InetAddress ia, int i1) throws IOException, UnknownHostException {

    }

    @Override
    public Socket createSocket(InetAddress ia, int i) throws IOException {

    }

    @Override
    public Socket createSocket(InetAddress ia, int i,
            InetAddress ia1, int i1) throws IOException {

    }
}

Nun müssen wir diesen Methoden Leben einhauchen, aber keine Angst das ist nicht so schlimm wie es aussieht, denn die einzige Änderung die wir machen müssen ist es der SSLSocktFactory beizubringen mit unserem Truststore zu arbeiten, der Rest der Funktionalität soll unberührt bleiben.

Fügen wir also einen Konstruktor zu unserer Klasse hinzu der das Initialisieren unserer Internen SSLSocketFactory mit unserem Truststore übernimmt.

/** Delegate {@link SSLSocketFactory}. */
private transient SSLSocketFactory factory;

private File caFile;
private KeyStore ks;

private SSLContext context;
private TrustManagerFactory tmf;
private X509TrustManager defTrustManager;
private SavingTrustManager tm;

/**
 * Neue Truststore-basierte SSL-SocketFactory
 * @param truststoreLocation Vollqualifizierter Pfad zum Truststore oder
			leer lassen für Java-Default-Truststore
 * @param truststorePass Passwort für den Truststore. Wenn leer wird
			"changeit" ala Passwort benutzt.
 */
public ServerSSLAuthSocketFactory(String truststoreLocation,
								  String truststorePass){
	super();

	//Wenn ein Pfad zum TrustStore angegeben wurde diesen benutzen
	if(!truststoreLocation.isEmpty()){
		caFile = new File(truststoreLocation);
	}else{
		//Pfad zum default Java TrustStore finden
		caFile = new File("cacerts");
		if (!caFile.exists() || !caFile.isFile()) {
			char SEP = File.separatorChar;
			File dir = new File(System.getProperty("java.home") + SEP
					+ "lib" + SEP + "security");
			caFile = new File(dir, "cacerts");
			if (!caFile.exists() || !caFile.isFile()) {
				caFile = new File(dir, "cacerts");
			}
		}
	}

	//Prüfen, ob ein TrustStore Password angegeben wurde
	String truststorePassword = truststorePass.isEmpty() ? "changeit" : truststorePass;

	//TrustStore öffnen
	try {
		InputStream in = new FileInputStream(caFile);
		ks = KeyStore.getInstance(KeyStore.getDefaultType());
		ks.load(in, truststorePassword.toCharArray());
		in.close();
	} catch (KeyStoreException ex) {
		Logger.getLogger(ServerSSLAuthSocketFactory.class.getName())
						.log(Level.SEVERE, null, ex);
	} catch (NoSuchAlgorithmException ex){
		Logger.getLogger(ServerSSLAuthSocketFactory.class.getName())
						.log(Level.SEVERE, null, ex);
	} catch (CertificateException ex) {
		Logger.getLogger(ServerSSLAuthSocketFactory.class.getName())
						.log(Level.SEVERE, null, ex);
	} catch (FileNotFoundException ex) {
		Logger.getLogger(ServerSSLAuthSocketFactory.class.getName())
						.log(Level.SEVERE, null, ex);
	}catch (IOException ex) {
		Logger.getLogger(ServerSSLAuthSocketFactory.class.getName())
						.log(Level.SEVERE, null, ex);
	}

	try {
		context = SSLContext.getInstance("TLS");
		tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
		tmf.init(ks);
		defTrustManager = (X509TrustManager)tmf.getTrustManagers()[0];
		context.init(null, new TrustManager[] {defTrustManager}, null);
		factory = context.getSocketFactory();
	} catch (NoSuchAlgorithmException ex) {
		Logger.getLogger(ServerSSLAuthSocketFactory.class.getName())
						.log(Level.SEVERE, null, ex);
	} catch (KeyStoreException ex) {
		Logger.getLogger(ServerSSLAuthSocketFactory.class.getName())
						.log(Level.SEVERE, null, ex);
	} catch (KeyManagementException ex) {
		Logger.getLogger(ServerSSLAuthSocketFactory.class.getName())
						.log(Level.SEVERE, null, ex);
	}

}

Das sieht auf den ersten Blick recht komplex aus, ist aber im Grunde recht simpel.

  • In den Zeilen 23 – 28 prüfen wir ob der Konstruktor einen Pfad zu einem Truststore übergeben bekommen hat, und wenn dies nicht der Fall ist, wird der Pfad zu dem Standard-Java-Truststore in eurem JRE-Verzeichnis ermittelt.
  • Direkt danach in Zeile 41 prüfen wir ob ein Passwort übergeben wurde. Wenn keins übergeben wurde wird „changeit“ genutzt. „changeit“ ist das Defaultpasswort für den Standard-Java-Truststore.
  • In Zeile 45 – 48 öffnen wir den Keystore und laden dessen Inhalt in unsere Kokale KeyStore Variable.
  • Das Wichtigste geschieht nun in Zeile 67 – 72 hier holen wir uns einen Verweis auf den SSLContext (67) und erstellen uns mit Hilfe unseres KeyStores einene TrustManagerFactory(68-69). Anschließend lassen wir die TrustManagerFactory für uns einen neuen X509TrustManager „produzieren“ der dann den Verweis auf unseren eigenen Keystore trägt. (70)
  • Nun sagen wir dem SSLContext, dass es unseren Trustmanager nutzen soll (71) für die darüber laufende Verbindungen und erstellen uns eine SSLSocketFactory (72)

Nun haben wir intern eine SocketFactory, die unseren eigenen (über den Konstruktor angegebenen) Keystore nutzt. Der Rest der Klasse die wir erstellt haben dient hierbei eigentlich nur als Wrapper und Durchreiche, der Wichtigste Teil war der Konstruktor.

Die restlichen Methoden können nun also alle Anfragen direkt an die produzierte SSLSocketFactory weiterleiten. So sieht dann die fertige Klasse aus:

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.net.InetAddress;
import java.net.Socket;
import java.net.UnknownHostException;
import java.security.KeyManagementException;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLSocket;
import javax.net.ssl.SSLSocketFactory;
import javax.net.ssl.TrustManager;
import javax.net.ssl.TrustManagerFactory;
import javax.net.ssl.X509TrustManager;

/**
 * Stellt eine SSLSocketFactory bereit die einen angegebenen TrustStore nutzt
 * @author Sebastian Gross
 */
public class ServerSSLAuthSocketFactory extends SSLSocketFactory{

    /** Delegate {@link SSLSocketFactory}. */
    private transient SSLSocketFactory factory;

    private File caFile;
    private KeyStore ks;

    private SSLContext context;
    private TrustManagerFactory tmf;
    private X509TrustManager defTrustManager;
    private SavingTrustManager tm;

    /**
     * Neue Truststore-basierte SSL-SocketFactory
     * @param truststoreLocation Vollqualifizierter Pfad zum
	 *			Truststore oder leer lassen für Java-Default-Truststore
     * @param truststorePass Passwort für den Truststore.
	 *			Wenn leer wird "changeit" ala Passwort benutzt.
     */
    public ServerSSLAuthSocketFactory(String truststoreLocation,
				String truststorePass){
        super();

        //Wenn ein Pfad zum TrustStore angegeben wurde diesen benutzen
        if(!truststoreLocation.isEmpty()){
            caFile = new File(truststoreLocation);
        }else{
            //Pfad zum default Java TrustStore finden
            caFile = new File("cacerts");
            if (!caFile.exists() || !caFile.isFile()) {
                char SEP = File.separatorChar;
                File dir = new File(System.getProperty("java.home") + SEP
                        + "lib" + SEP + "security");
                caFile = new File(dir, "cacerts");
                if (!caFile.exists() || !caFile.isFile()) {
                    caFile = new File(dir, "cacerts");
                }
            }
        }

        //Prüfen, ob ein TrustStore Password angegeben wurde
        String truststorePassword =
				truststorePass.isEmpty() ? "changeit" : truststorePass;

        //TrustStore öffnen
        try {
            InputStream in = new FileInputStream(caFile);
            ks = KeyStore.getInstance(KeyStore.getDefaultType());
            ks.load(in, truststorePassword.toCharArray());
            in.close();
        } catch (KeyStoreException ex) {
            Logger.getLogger(ServerSSLAuthSocketFactory.class.getName())
						.log(Level.SEVERE, null, ex);
        } catch (NoSuchAlgorithmException ex){
            Logger.getLogger(ServerSSLAuthSocketFactory.class.getName())
						.log(Level.SEVERE, null, ex);
        } catch (CertificateException ex) {
            Logger.getLogger(ServerSSLAuthSocketFactory.class.getName())
						.log(Level.SEVERE, null, ex);
        } catch (FileNotFoundException ex) {
            Logger.getLogger(ServerSSLAuthSocketFactory.class.getName())
						.log(Level.SEVERE, null, ex);
        }catch (IOException ex) {
            Logger.getLogger(ServerSSLAuthSocketFactory.class.getName())
						.log(Level.SEVERE, null, ex);
        }

        try {
            context = SSLContext.getInstance("TLS");
            tmf = TrustManagerFactory.getInstance(TrustManagerFactory
													.getDefaultAlgorithm());
            tmf.init(ks);
            defTrustManager = (X509TrustManager)tmf.getTrustManagers()[0];
            context.init(null, new TrustManager[] {defTrustManager}, null);
            factory = context.getSocketFactory();
        } catch (NoSuchAlgorithmException ex) {
            Logger.getLogger(ServerSSLAuthSocketFactory.class.getName())
						.log(Level.SEVERE, null, ex);
        } catch (KeyStoreException ex) {
            Logger.getLogger(ServerSSLAuthSocketFactory.class.getName())
						.log(Level.SEVERE, null, ex);
        } catch (KeyManagementException ex) {
            Logger.getLogger(ServerSSLAuthSocketFactory.class.getName())
						.log(Level.SEVERE, null, ex);
        }

    }

    @Override
    public String[] getDefaultCipherSuites() {
        return factory.getDefaultCipherSuites();
    }

    @Override
    public String[] getSupportedCipherSuites() {
        return factory.getSupportedCipherSuites();
    }

    @Override
    public Socket createSocket(Socket socket, String string,
					int i, boolean bln) throws IOException {
        return factory.createSocket(socket, string, i, bln);
    }

    @Override
    public Socket createSocket(String destinationAddress,
					int destinationPort) throws IOException, UnknownHostException {
        return (SSLSocket)factory.createSocket(destinationAddress, destinationPort);
    }

    @Override
    public Socket createSocket(String string, int i,
					InetAddress ia, int i1) throws IOException, UnknownHostException {
        return factory.createSocket(string, i, ia, i1);
    }

    @Override
    public Socket createSocket(InetAddress ia, int i) throws IOException {
        return factory.createSocket(ia, i);
    }

    @Override
    public Socket createSocket(InetAddress ia, int i,
					InetAddress ia1, int i1) throws IOException {
        return factory.createSocket(ia, i, ia1, i1);
    }
}

Da wir nun eine Factory haben die uns Sockets liefert, die auf unseren Truststore zugreift, können wir diese nun nutzen um eine Verbindung zu SSL Servern aufzubauen. Das Schöne hierbei ist, dass Java an sehr vielen Stellen SocketFactories nutzt, so können wir fast in allen Szenarien unsere Klasse einsetzen.

So können wir unsere SSLFactory nun ganz simpel testen:

404 Not Found

Error: Not Found

The requested URL / was not found on this server.

public static void main (String [] args) throws IOException{ int port = 443; // default https port String host = "httpsurl"; try { SSLSocketFactory factory = (SSLSocketFactory) new ServerSSLAuthSocketFactory("",""); SSLSocket socket = (SSLSocket) factory.createSocket(host, port); // enable all the suites String[] supported = socket.getSupportedCipherSuites(); socket.setEnabledCipherSuites(supported); Writer out = new OutputStreamWriter(socket.getOutputStream()); // https requires the full URL in the GET line out.write("GET https://" + host + "/ HTTP/1.1\r\n"); out.write("Host: " + host + "\r\n"); out.write("\r\n"); out.flush(); // read response BufferedReader in = new BufferedReader( new InputStreamReader(socket.getInputStream())); // read the header String s; while (!(s = in.readLine()).equals("")) { System.out.println(s); } System.out.println(); // read the length String contentLength = in.readLine(); int length = Integer.MAX_VALUE; try { length = Integer.parseInt(contentLength.trim(), 16); } catch (NumberFormatException ex) { // This server doesn't send the content-length // in the first line of the response body } System.out.println(contentLength); int c; int i = 0; while ((c = in.read()) != -1 && i++ < length) { System.out.write(c); } System.out.println(); out.close(); in.close(); socket.close(); } catch (IOException ex) { System.err.println(ex); } } [/code]

Tragt in Zeile 4 einfach den gewünschten SSL-Host ein wie z.B. Google oder Facebook und startet das kleine Programm. Falls ihr keine Parameter beim Erzeugen der Factory (Zeile 7 und 8) angegeben habt solltet ihr auch keine Probleme beim Verbinden haben und wenn das Programm durchgelaufen ist seht ihr die Antwort des Servers im HTML Format.

Falls ihr doch einen eigenen Truststore eingetragen habt müsst ihr sicherstellen, dass dieser den Public Key des Servers zu dem ihr euch verbinden wollt beinhaltet, sonst wird die Verbindung nicht erfolgen.

Teil 1: Serverseitige Authentifizierung und Zertifikate
Teil 2: Beidseitige Authentifizierung
Teil 3: Der SSL Handshake
Teil 4: Serverseitige Authentifizierung mit Java

profile

Sebastian Gross

http://www.bigbasti.com

Sebastian Gross arbeitet in Bielefeld als Softwareentwickler für .NET und Java im Bereich Web.Als Fan der .NET-Plattform lässt er sich kein Userguppen Treffen und Community Event im Raum OWL entgehen.Dabei hat er eine besondere Vorliebe für das ASP.NET MVC Framework und für das Test Driven Development (TDD) entwickelt.

There are no comments.

Kommentar verfassen

View Comments (0) ...
Navigation