security - JAX https://jax.de/tag/security/ Java, Architecture & Software Innovation Wed, 02 Oct 2024 13:38:12 +0000 de-DE hourly 1 https://wordpress.org/?v=6.5.2 Java-Anwendungen mit Bordmitteln absichern https://jax.de/blog/java-anwendungen-mit-bordmitteln-absichern/ Tue, 03 Jan 2023 14:26:20 +0000 https://jax.de/?p=88195 Aufgabe der IT-Sicherheit ist es vor allem, nicht autorisierte Handlungen von Benutzern eines Computersystems zu verhindern und zu ermitteln. Sie befasst sich insbesondere mit Maßnahmen, die auf absichtliche Handlungen unterschiedlicher Parteien abzielen [1]. IT-Security ist immens wichtig geworden: Im Jahr 2021 wurde der Markt auf knapp 140 Milliarden US-Dollar beziffert; für das Jahr 2029 wird mit bis zu 376 Milliarden US-Dollar Umsatz gerechnet [2]. Folglich sind auch Fachkräfte wie IT-Sicherheitsingenieure oder -analysten sehr gefragt [3].

The post Java-Anwendungen mit Bordmitteln absichern appeared first on JAX.

]]>
Sein eigenes Sicherheitssystem zu basteln, birgt die Gefahr potenzieller Schwächen, die Hacker aufspüren können. Beispielsweise werden Passwörter als Klartext gespeichert oder versendet, die Leitung zwischen Client und Server ist nicht geschützt oder die Entwickler haben gar einen eigenen Verschlüsselungsalgorithmus beziehungsweise ihr eigenes Session-Management entwickelt.

Falls ihr keine IT-Sicherheitsexperten seid, solltet ihr daher lieber auf Altbewährtes setzen. Die Sicherheitsarchitektur von Java, genannt Java SE, ist eine gute Wahl [4]. Sie beinhaltet eine große Zahl an Schnittstellen, Tools und Unterstützung für häufig genutzte Sicherheitsalgorithmen, -mechanismen und -protokolle. Mit der Java-Sicherheitsarchitektur könnt ihr folgende Aufgaben realisieren:

  • Kryptografie

  • Öffentliche Schlüssel generieren

  • Sichere Kommunikation

  • Authentifizierung

  • Zugriffskontrolle

Java SE bringt zudem Erweiterungen sowie Dienste mit, die beim Entwickeln einer sicheren Anwendung unterstützen. Hierzu zählen insbesondere:

  • JSSE (Java Secure Socket Extension): stellt SSL-bezogene Dienste zur Verfügung

  • JCE (Java Cryptography Extension): kryptografische Dienste

  • JAAS (Java Authentication and Authorization Service): Dienste, die sich mit Zugangskontrollen und Authentifizierung beschäftigen

Um eine Anwendung auf sicherheitsbezogene Ereignisse hin zu untersuchen, übergebt ihr dem Java-Befehl das folgende Argument:

-Djava.security.debug

Authentifizierung

Derzeit stehen drei Module für die Authentifizierung unter Java zur Verfügung:

  • Krb5LoginModule: Authentifizierung mittels Kerberos-Protokoll

  • JndiLoginModule: Anmeldung mit Benutzername und Passwort, indem LDAP- oder NIS- Datenbanken zum Einsatz kommen

  • KeyStoreLoginModule: Anmeldung am PKCS-#11-Schlüsselspeicher

Falls ihr euch für das geläufige Verfahren mit Benutzername und Passwort entscheidet, um die Anmeldung der Benutzer zu sichern, dann benutzt am besten das JndiLoginModule. Hierbei könnt ihr auf die Klasse LoginContext zurückgreifen, die ihr wie folgt initialisiert [5]:

LoginContext loginContext = new LoginContext("Sample", new SampleCallbackHandler());
loginContext.login();

Wichtig ist, dass ihr dem Log-in-Handler einen Konfigurationsnamen sowie den CallbackHandler übergebt. Der CallbackHandler legt fest, ob die Eingabeaufforderungen für den Benutzernamen und das Kennwort nacheinander erscheinen oder beides in einem einzigen Fenster eingeblendet wird [6]. In der Konfigurationsdatei jaas.conf gebt ihr dann das erforderliche Log-in-Modul ein, mit dem eure Anwendung die Authentifizierung vornimmt. In der Regel lässt sich das Log-in-Modul mit nur wenigen Zeilen konfigurieren:

Sample {
  com.sun.security.auth.module.JndiLoginModule required;
};

Beim Starten der Java-Anwendung sind weitere Argumente erforderlich. So müssen der SecurityManager ex-tra aufgerufen und der Pfad zur Konfigurationsdatei jaas.conf angegeben werden:

java -Djava.security.manager -Djava.security.auth.login.config=/Pfad/zu/jaas.conf

Wem das nicht ausreicht, der kann auf weitere vordefinierte Log-in-Module [7] der Software AG zurückgreifen [8]. Zudem existieren Bibliotheken, die eine Implementierung mit geringerer Komplexität erlauben. So basiert etwa Apache Shiro auf der Java Security, vereinfacht jedoch die Konfiguration sowie Umsetzung von Authentifizierung, Autorisierung, Kryptografie und Benutzermanagement [9].

Stay tuned

Regelmäßig News zur Konferenz und der Java-Community erhalten

 

Zugriffskontrolle

Aber auch mit der Java Security kann eine Autorisierung für Benutzer implementiert werden. Hierbei ist eine Konfigurationsdatei mit der Endung .policy erforderlich, die sich mit policytool aus der JDK-Umgebung erstellen lässt (Abb. 1). Laut der Dokumentation des Java Security API läuft policytool zwar demnächst ab, doch gibt es derzeit keinen guten Ersatz dafür. policytool verfügt über eine grafische Oberfläche und lässt sich durch Eingabe des Befehls policytool auf der Konsole starten. Per Klick auf Policy-Eintrag hinzufügen öffnet sich ein neues Fenster, in dem sich unter anderem eine Berechtigung definieren lässt. Eine neue Berechtigung erzeugt man per Klick auf Berechtigung hinzufügen. Im Fenster Berechtigungen wird zunächst die Art der Berechtigung ausgewählt, in unserem Beispiel handelt es sich dabei um eine FilePermission, also Dateiberechtigung. Sie lässt sich auf eine bestimmte Datei oder auf alle Dateien unter Zielname anwenden. Bei Aktionen können zulässige Dateirechte wie lesen oder schreiben ausgewählt werden. Das Feld signiert von kann leer sein.

Abb. 1: Berechtigungen mit policytool erstellen

Nachdem alle Einstellungen vorgenommen wurden, kann die .policy-Datei mit Datei | Speichern abgespeichert werden:

/* AUTOMATICALLY GENERATED ON Mon Sep 12 18:46:47 CEST 2022*/
/* DO NOT EDIT */
grant {
  permission java.io.FilePermission "<<ALL FILES>>", "read,write";
};

Die konkrete Umsetzung der Zusatzkontrolle mit Java-Bordmitteln lässt sich bewerkstelligen wie in Listing 1 gezeigt. Wichtig ist, dem Java-Befehl beim Starten des Java-Programms folgende Argumente zu übergeben:

-Djava.security.manager -Djava.security.policy=/Pfad/zu/Datei.policy

Um die Ressourcen vom SecurityManager beschützen zu lassen, muss dieser zunächst initialisiert werden. Anschließend könnt ihr mittels der Methode securityManager.checkPermission überprüfen, ob beispielsweise ein Benutzer eine Datei löschen darf.

SecurityManager SecurityManager = System.getSecurityManager();
String path = "files/test.txt";
try {
  if (securityManager != null) {
    securityManager.checkPermission(new FilePermission(path, "delete"));
    System.out.println("it seems as if you're allowed to delete it");
  }
} catch (AccessControlException e) {
  System.out.println("Not allowed to perform this action: "+e.getMessage());
}

Geheimschlüssel

Werden Nachrichten, Benutzeranmeldedaten und weitere sensible Daten unverschlüsselt über das Netzwerk verschickt, haben Hacker leichtes Spiel. Die Kryptografie wird in Java daher sehr ernst genommen. Speziell das Framework Java Cryptography Architecture (JCA) wurde daher in die JDK- beziehungsweise JRE-Umgebung integriert [10]. Das JCA-Framework kann nützlich sein, wenn ihr euch unter anderem mit den folgenden Themen beschäftigen wollt:

  • digitale Signaturen

  • Message Digests (MD): kryptografische Hashfunktion [11]

  • Zertifikate sowie Zertifikatsvalidierung

  • Verschlüsselung (symmetrische/asymmetrische Block- und Stromchiffren)

  • Generierung sowie Verwaltung von Schlüsseln

  • sichere Zufallszahlengenerierung

Verschlüsselungsalgorithmen, die die Vertraulichkeit von Daten schützen, können entweder symmetrischer oder asymmetrischer Natur sein. So handelt es sich bei Geheimschlüsseln (Secret Keys) um symmetrische Algorithmen, da derselbe Schlüssel sowohl zur Verschlüsselung als auch zur Entschlüsselung benutzt wird. Anders verhält es sich bei asymmetrischen Algorithmen, bei denen unterschiedliche Schlüssel für die Verschlüsselung und die Entschlüsselung zum Einsatz kommen [1].

SIE LIEBEN JAVA?

Den Core-Java-Track entdecken

 

Ein Beispiel für symmetrische Algorithmen stellt der „Data Encryption Standard“ (DES) dar. Obwohl DES bereits in den 1970ern entwickelt und mittlerweile von diversen fortgeschrittenen Standards überholt wurde, ist er immer noch in Gebrauch. DES kommt vor allem im kommerziellen sowie im Finanzsektor zum Einsatz. Die eigentliche Schlüsselgröße von DES beträgt zwar 56 Bit, allerdings hat sich heutzutage vor allem die Option Triple DES mit einer Schlüsselgröße von 3 x 56 Bit durchgesetzt [1].

import java.security.spec.KeySpec;
import javax.crypto.Cipher;
import javax.crypto.SecretKey;
import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.DESedeKeySpec;
import org.apache.commons.codec.binary.Base64;
 
public class TrippleDes {
 
  private static final String UNICODE_FORMAT = "UTF8";
  public static final String DESEDE_ENCRYPTION_SCHEME = "DESede";
  private KeySpec ks;
  private SecretKeyFactory skf;
  private Cipher cipher;
  byte[] arrayBytes;
  private String myEncryptionKey;
  private String myEncryptionScheme;
  SecretKey key;
 
  public TrippleDes(String sKey) throws Exception {
    myEncryptionKey = sKey;
    myEncryptionScheme = DESEDE_ENCRYPTION_SCHEME;
    arrayBytes = myEncryptionKey.getBytes(UNICODE_FORMAT);
    ks = new DESedeKeySpec(arrayBytes);
    skf = SecretKeyFactory.getInstance(myEncryptionScheme);
    cipher = Cipher.getInstance(myEncryptionScheme);
    key = skf.generateSecret(ks);
  }
  public String encrypt(String unencryptedString) {
    String encryptedString = null;
    try {
      cipher.init(Cipher.ENCRYPT_MODE, key);
      byte[] plainText = unencryptedString.getBytes(UNICODE_FORMAT);
      byte[] encryptedText = cipher.doFinal(plainText);
      encryptedString = new String(Base64.encodeBase64(encryptedText));
    } catch (Exception e) {
      e.printStackTrace();
    }
    return encryptedString;
  }
 
  public String decrypt(String encryptedString) {
    String decryptedText=null;
    try {
      cipher.init(Cipher.DECRYPT_MODE, key);
      byte[] encryptedText = Base64.decodeBase64(encryptedString);
      byte[] plainText = cipher.doFinal(encryptedText);
      decryptedText= new String(plainText);
    } catch (Exception e) {
      e.printStackTrace();
    }
    return decryptedText;
  }
}

Unter Java könnt ihr Triple DES implementieren wie in Listing 2 dargestellt [12]. Triple DES wird im Konstruktor eingerichtet, dem der Geheimschlüssel als Argument im Klartext übergeben wird. Im weiteren Verlauf der Einrichtung kommt die SecretKeyFactory zum Einsatz, um die Byterepräsentation des Geheimschlüssels in ein SecretKey-Objekt umzuwandeln. Anhand der Implementierung der encrypt– und decrypt-Methoden lässt sich gut erkennen, dass die Entschlüsselung eine Umkehrfunktion der Verschlüsselung ist. Anschließend könnt ihr einen String wie folgt ver- und entschlüsseln:

TrippleDes triple = new TrippleDes("Caesarsalad!Caesarsalad!");
String encPw = triple.encrypt("password123");
String decPw = triple.decrypt(encPw);

Abhörsichere Leitung

Vertrauliche Kommunikation über einen Kanal wie das öffentliche Internet erfordert die Verschlüsselung von Daten. Aus diesem Grund ist es absolut notwendig, das Verschlüsselungsprotokoll Transport Layer Security (TLS), ehemals bekannt als Secure Socket Layer (SSL), einzusetzen. Andernfalls könnte ein Hacker in Versuchung geraten, einen Packet Sniffer einzusetzen, der den Netzwerkverkehr analysiert. Beispielsweise lassen sich sensible Daten wie Kreditkartennummern vor dem Versand unverschlüsselt speichern. Die einfachste Lösung, um weder den Server noch den Client mit einem zu Fehlern neigenden Verschlüsselungscode zu überladen, besteht darin, Secure Sockets einzusetzen.

Als Beispiel soll eine verschlüsselte Kommunikation zwischen Server und Client dienen [13]. Beide verwenden einen Secure Socket, wobei die Daten in Form von JSON-Daten gesendet und empfangen werden. Wie sich der Server, bestehend aus einem Secure Socket, realisieren lässt, zeigt Listing 3. Falls der Server Teil einer Client-/Server-Anwendung ist, sollte er die Thread-Klasse erweitern. Dadurch wird die Anwendung multitaskingfähig. So kann der Server parallel mit den angeschlossenen Clients kommunizieren, während er weitere Aufgaben erledigt.

import java.io.*;
import java.net.*;
import java.security.KeyManagementException;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.UnrecoverableKeyException;
import java.security.cert.CertificateException;
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicBoolean;
import javax.net.*;
import javax.net.ssl.KeyManagerFactory;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLServerSocket;
import javax.net.ssl.SSLServerSocketFactory;
 
public class ServerConnection extends Thread {
  private final int port;
  private AtomicBoolean running = new AtomicBoolean(false);
  private ExecutorService pool;
 
  public ServerConnection(int p) {
    this.port = p;
    pool = Executors.newFixedThreadPool(10000);
  }
 
  public void run() {
    running.set(true);
    this.setupSecureSocket();
  }
 
  public void setupSecureSocket() {
    try {
      ServerSocketFactory factory = ServerSocketFactory.getDefault();
      ServerSocket socket = factory.createServerSocket(port);
      System.out.println("creating server socket");
      while (running.get()) {
        try {
          Socket s = socket.accept();
          Runnable r = new ServerThread(s);
          pool.submit(r);
          
        } catch (IOException ex) {
          System.out.println("Exception accepting connection");
        } catch (RuntimeException ex) {
          System.out.println("Unexpected error"+ex.getMessage());
        }
      }
      System.out.println("shutting down server");
      pool.shutdown();
      socket.close();
      
    } catch (BindException ex) {
      System.out.println("Could not start server. "+ex.getMessage());
    } catch (IOException ex) {
      System.out.println("Error opening server socket: "+ex.getMessage());
    }
  }
  
  public void interrupt() {
    running.set(false);
  }
  
  private boolean isRunning() {
    return running.get();
  }
}

In Listing 3 wird der Server initialisiert, indem die Portnummer als Argument übergeben wird. Außerdem wird mittels des ExecutorService-Objekts die Zahl der Threads festgelegt, die der Server verarbeiten kann. So können in diesem Beispiel bis zu 10 000 Clients mit dem Server verbunden werden.

Da die Klasse ServerConnection von der Thread-Klasse die Methoden und Attribute erbt, ist es zusätzlich erforderlich, die bekannte run-Methode zu überschreiben. In diesem Fall reicht es aus, die selbst definierte Methode setupSecureSocket() aufzurufen. Dort wird zunächst der Secure Socket des Servers initialisiert, was dazu führt, dass der Server auf einem sicheren Kanal mit der Portnummer XY lauscht. Solange die Variable running vom Typ AtomicBoolean den Wert true aufweist, kann der Server neue Clients zulassen, indem jeder Client als ein Runnable-Objekt zum Pool hinzugefügt wird. Die Klasse ServerConnection stellt weitere Hilfsmethoden zur Verfügung, mit denen sich der Server steuern lässt. So kann mittels Aufruf der Methode interrupt() jederzeit das Verwalten der Clients unterbrochen werden. Der Aufruf der Methode isRunning() zeigt an, ob der Server noch läuft.

Stay tuned

Regelmäßig News zur Konferenz und der Java-Community erhalten

 

Bei der Anmeldung am Server wird jedem Client ein ServerThread-Objekt vom Typ Runnable zugeordnet (Listing 4). Die Implementierung der Runnable-Schnittstelle sorgt dafür, dass die run-Methode nicht mehr explizit aufgerufen werden muss. Zusätzlich wird beim Anlegen des ServerThread-Objekts der Socket des Clients übergeben, um damit den BufferedWriter sowie InputStreamReader zu initialisieren.

Der Aufruf von start() erfolgt in der überschriebenen run-Methode und sorgt dafür, dass der booleschen Variable shutdown der Wert false zugewiesen wird. Durch den anschließenden Aufruf von getIncomingData() wird der InputStreamReader gestartet, sodass in der while-Schleife konstant Nachrichten vom Client empfangen und zu einem JSON-Objekt geparst werden. Daneben beinhaltet die ServerThread-Klasse die Methode disconnect(), die die Verbindung zum Client kappt. Hierbei wird die boolesche Variable shutdown auf true gesetzt, was dazu führt, dass die while-Schleife unterbrochen wird.

import org.json.simple.JSONObject;
import org.json.simple.parser.JSONParser;
import org.json.simple.parser.ParseException;
import java.net.Socket;
import java.io.Writer;
import java.io.BufferedWriter;
import java.io.OutputStreamWriter;
import java.io.InputStreamReader;
import java.io.IOException;
import java.io.BufferedInputStream;
import java.io.Reader;
import java.io.BufferedReader;
 
public class ServerThread implements Runnable {
  private final Socket connection;
  private Writer out;
  private Reader in;
  private BufferedReader reader;
  private volatile boolean shutdown;
 
  public ServerThread(Socket s) {
    this.connection = s;
    
  }
 
  public void run() {
  
    try {
      this.out = new BufferedWriter(new OutputStreamWriter(connection.getOutputStream(), "US-ASCII"));
      
      System.out.println("client address: " + this.connection.getRemoteSocketAddress());
      {
        this.start();
        getIncomingData();
      } catch(IOException ex) {
        System.out.println("Error talking to " + this.connection.getRemoteSocketAddress()+" "+ex.getMessage());
      }
    }
    
    private void getIncomingData() {
      JSONParser parser = new JSONParser();
      
      try {
        this.in = new InputStreamReader(new BufferedInputStream(connection.getInputStream()));
        this.reader = new BufferedReader( this.in);
        String line = "";
        while(!shutdown){
          if ((line =  this.reader.readLine()) != null) {
            System.out.println("line: "+line);
            JSONObject json = (JSONObject) parser.parse(line);
            System.out.println("reading in json "+json);
          } else {
            this.in.close();
            disconnect();
          }
        }
        
      } catch(IOException ex) {
        System.out.println("could not read in data: " + ex.getMessage());
      } catch(ParseException ex) {
        System.out.println("could not parse JSONObject: " + ex.getMessage());
      }
    }
    
    public void disconnect() {
      try {
        this.connection.close();
        shutdown();
        System.out.println("closing client socket ");
      } catch (IOException ex) {
        System.out.println("could not close client socket " + ex.getMessage());
      }
    }
    
    public void shutdown() {
      shutdown = true;
    }
    
    public void start() {
      shutdown = false;
    }
  }
}

Auch beim Client muss ein Secure Socket erstellt werden, um darüber eine Verbindung zum Server aufzubauen. Die Umsetzung des Clients erfolgt durch die Klasse ClientConnection (Listing 5). So werden beim Erstellen des ClientConnection-Objekts sowohl die Portnummer als auch der Hostname des Servers als Argumente übergeben.

Das Herzstück der ClientConnection ist die connect-Methode, da dort der Secure Socket mittels SSLSocketFactory eingerichtet wird. Zudem werden dort sowohl der BufferedWriter, der zum Schreiben von Daten an den Server gebraucht wird, als auch der InputStream, über den Daten vom Server empfangen werden, initialisiert. Erwähnenswert ist in diesem Zusammenhang die Methode disconnect(), welche die Verbindung zum Server beendet. Durch den Aufruf von write() können Stringnachrichten an den Server verschickt werden.

import java.io.BufferedWriter;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.Writer;
import java.net.Socket;
import java.util.concurrent.atomic.AtomicBoolean;
import javax.net.SocketFactory;
import javax.net.ssl.SSLSocketFactory;
 
public class ClientConnection implements Runnable {
  private Socket socket;
  private OutputStream out;
  private Writer writer;
  private InputStream in;
  private String host = "";
  private int port = 0;
  private AtomicBoolean running = new AtomicBoolean(false);
 
  public ClientConnection(String host, int p) {
    this.host = host;
    this.port = p;
  }
  
  public void run() {
    read();
  }
 
  public void connect() {
    try {
      SocketFactory factory = SSLSocketFactory.getDefault();
      this.socket = factory.createSocket(host, port);
      this.socket.setSoTimeout(100000);
      this.out = this.socket.getOutputStream();
      this.writer = new BufferedWriter(new OutputStreamWriter(this.out, "UTF-8"));
      this.in = this.socket.getInputStream();
      System.out.println("Could establish connection to server.");
    } catch (IOException ex) {
      System.out.println("Could not connect to server."+ex.getMessage());
    }
  }
  
  public void write(String msg) {
    try {
      if(this.writer != null) {
      this.writer.write(msg);
      this.writer.flush();
    } else {
      System.out.println("Could not write to server.");
    }
  } catch (IOException ex) {
    System.out.println("Could not write to server. "+ex.getMessage());
  }
}
 
public void disconnect() {
  if (this.socket != null) {
    try {
      this.socket.close();
      System.out.println("disconnected from server");
    } catch (IOException ex) {
      System.out.println("could not disconnect.");
    }
  }
}
 
public boolean isConnected() {
  if ( this.socket != null ) {
    System.out.println("connected to server");
    return true;
  } else {
    System.out.println("not connected to server");
    return false;
  }
}
 
public void read() {
  //TODO: read in from server
  }
}

Die hier vorgestellte Client-/Serveranwendung kann anschließend in der Main-Methode aufgerufen werden. Den Server initialisiert ihr dabei unter Angabe der Portnummer wie folgt:

ServerConnection server = new ServerConnection(9200);
server.run();

Genauso wird der Client gestartet, außer das hier noch ein kleiner Test stattfindet. So versendet der Client persönliche Daten als JSON-String (Listing 6).

ClientConnection client = new ClientConnection("127.0.0.1", 9200);
client.connect();
 
JSONObject jObj = new JSONObject();
jObj.put("Name", "Max Mustermann");
jObj.put("Product-ID", "67x-89");
jObj.put("Address", "1280 Deniston Blvd, NY NY 10083");
jObj.put("Card Number", "4000-1234-5678-9017");
jObj.put("Expires", "10/29");
try {
  TimeUnit.SECONDS.sleep(30);
} catch (InterruptedException e) {
  e.printStackTrace();
}
client.writeToServer(jObj.toJSONString()+"\n");

Trotz des einfach gehaltenen Codes werden die vertraulichen Daten verschlüsselt an den Server gesendet. Überprüfen könnt ihr das unter Linux mit dem Kommandozeilentool Tcpflow [14]. Denn Tcpflow ist eine Art Packet Sniffer, der den gesamten oder auch einen Teil des TCP-Verkehrs aufzeichnet und in eine einfache Datei schreibt. Um beispielsweise den Port mittels Tcpflow abzuhören, der für dieses Beispielprogramm benutzt wird, verwendet ihr folgenden Befehl:

tcpflow -i any port 9200

Beim Aufruf von Tcpflow werden ein oder mehrere Dateien angelegt, welche die Daten beinhalten, die zwischen Client und Server gesendet worden sind. Dabei ähneln die Dateinamen dem folgenden Beispiel:

127.000.000.001.38226-127.000.000.001.09200

Unschwer zu erkennen ist, dass Daten vom Client mit der Portnummer 38226 an den Server mit der Portnummer 9200 gesendet worden sind. Schaut ihr euch allerdings den aufgezeichneten Netzwerkverkehr im Texteditor an, so werdet ihr abgesehen von kryptischen Symbolen keinerlei persönliche Daten entdecken (Abb. 2). Allerdings kann auch der implementierte Server mit den verschlüsselten Daten nichts anfangen, was daran liegt, dass der Server nicht weiß, welches Verschlüsselungsverfahren zum Einsatz kommt.

Abb. 2: Aufzeichnung verschlüsselter Daten

Zertifikatehandel

Der Server aus Listing 3 hat zwar den Vorteil, dass er relativ unkompliziert umsetzbar ist, allerdings unterstützt die factory, die von ServerSocketFactory.getDefault() zurückgegeben wird, lediglich die Authentifizierung [15]. Um die Verschlüsselung serverseitig zu implementieren, sind mehr Codezeilen nötig, was die Implementierung des Servers komplexer macht.

Zunächst legt ihr einen Keystore, einen Truststore sowie ein Zertifikat für den Server an. Genauso geht ihr beim Client vor, was allerdings nicht erforderlich ist, falls der Client allen Zertifikaten des Servers vertraut. Unter Linux stehen euch für solche Zwecke die Kommandozeilentools keytool sowie openssl zur Verfügung [16]:

  1. privaten RSA-Schlüssel generieren: $ openssl genrsa -out diagserverCA.key 2048

  2. x509-Zertifikat erstellen: $ openssl req -x509 -new -nodes -key diagserverCA.key -sha256 -days 1024 -out diagserverCA.pem

  3. PKCS12 Keystore anhand des zuvor generierten Privatschlüssels und Zertifikats erzeugen: $ openssl pkcs12 -export -name server-cert -in diagserverCA.pem -inkey diagserverCA.key -out serverkeystore.p12

  4. PKCS12-Keystore in einen Keystore vom Format JKS konvertieren: $ keytool -importkeystore -destkeystore server.keystore -srckeystore serverkeystore.p12 -srcstoretype pkcs12 -alias server-cert

  5. falls der Client mit Zertifikaten arbeitet, das Clientzertifikat zum Truststore des Servers hinzufügen: $ keytool -import -alias client-cert -file ../client/diagclientCA.pem -keystore server.truststore

  6. Serverzertifikat zum Truststore des Servers hinzufügen: keytool -import -alias server-cert -file diagserverCA.pem -keystore server.truststore

Anschließend ist es erforderlich, die Methode setup-SecureSocket() aus Listing 3 durch den Code aus Listing 7 zu ersetzen. Dabei definiert ihr zunächst den SSLContext (siehe setupSSLContext()), indem ihr eine TrustManagerFactory erstellt. Allerdings kann die TrustManagerFactory dem Wert 0 entsprechen, sofern eine KeyManagerFactory unter Angabe des Schlüsseltyps erzeugt wird. Dann erzeugt ihr ein KeyStore-Objekt vom Typ „JKS“ und befüllt es mit dem Server-Keystore, den ihr zuvor mit keytool erzeugt habt. Zusätzlich initialisiert ihr die KeyManagerFactory unter Angabe des Keystores und des dazugehörigen Passworts. Zu guter Letzt initialisiert ihr den SSLContext, indem ihr den Key-Manager vom Typ KeyManagerFactory, den Trust-Manager vom Typ TrustManagerFactory sowie eine sichere Zufallszahl, die SecureRandomNumber, dem SSLContext als Argumente übergebt. Die letzten beiden Argumente können null sein, falls die Standardeinstellungen ausreichen.

MEHR PERFORMANCE GEFÄLLIG?

Performance & Security-Track entdecken

 

Zusätzlich braucht der Server einen Secure-Server-Socket, um darüber mit den Clients zu kommunizieren. Hierfür kommt eine SSLServerSocketFactory zum Einsatz, womit sich unter Angabe der Portnummer ein SSLServerSocket-Objekt erstellen lässt (siehe setupSSLSocket()). Außerdem lässt sich der Server-Socket weiter einrichten. So brauchen sich Clients nicht zu authentifizieren (vgl. setNeedClientAuth()).

Die letzte Konfiguration betrifft die Verschlüsselungsalgorithmen, die der Server unterstützt. In der Methode setupCipher() werden zunächst die von der aktuellen Java-Umgebung unterstützten Verschlüsselungssuiten geladen. Danach wird durch all diese Verschlüsselungssuiten iteriert, indem nur jene ausgewählt werden, die anonyme und unbestätigte Verbindungen zum SSLServerSocket zulassen. Das ist dann der Fall, wenn im Namen der Suite der String anon vorkommt. Das neue Array wird anschließend der Methode setEnabled-CipherSuites() übergeben.

(..)
private static final String SERVER_KEYSTORE = "server/server.keystore";
(..)
 
public void setupSSLSocket() {
  SSLContext context = setupSSLContext();
  try {
    SSLServerSocketFactory factory = context.getServerSocketFactory();
    SSLServerSocket socket = (SSLServerSocket) factory.createServerSocket(port);
    socket.setNeedClientAuth(false);
    socket.setWantClientAuth(false);
    setupCipher(socket);
    System.out.println("creating server socket");
    while (running.get()) {
      try {
        Socket s = socket.accept();
        Runnable r = new ServerThread(s);
        pool.submit(r);
      } catch (IOException ex) {
        System.out.println("Exception accepting connection");
      } catch (RuntimeException ex) {
        System.out.println("Unexpected error"+ex.getMessage());
      }
    }
    System.out.println("shutting down server");
    pool.shutdown();
    socket.close();
  } catch (BindException ex) {
    System.out.println("Could not start server. "+ex.getMessage());
  } catch (IOException ex) {
    System.out.println("Error opening server socket: "+ex.getMessage());
  }
}
 
public SSLContext setupSSLContext() {
  SSLContext context = null;
  try {
    context = SSLContext.getInstance(“SSL”);
    KeyManagerFactory kmf =KeyManagerFactory.getInstance("SunX509");
    KeyStore ks = KeyStore.getInstance("JKS");
    char [] pw = "boguspw".toCharArray();
    ks.load( new FileInputStream(SERVER_KEYSTORE), pw);
    kmf.init(ks, pw);
    context.init(kmf.getKeyManagers(), null, null);
  } catch (NoSuchAlgorithmException e) {
    System.out.println("wrong algorithm: "+e.getMessage());
    e.printStackTrace();
  } catch (KeyStoreException e) {
    System.out.println("wrong keystore algo: "+e.getMessage());
    e.printStackTrace();
  } catch (CertificateException e) {
    System.out.println("certificate invalid: "+e.getMessage());
    e.printStackTrace();
  } catch (UnrecoverableKeyException e) {
    System.out.println("wrong password for keystore: "+e.getMessage());
    e.printStackTrace();
  } catch (KeyManagementException e) {
    System.out.println("could not initialize context: "+e.getMessage());
    e.printStackTrace();
  } catch (FileNotFoundException e) {
    System.out.println("file not found: "+e.getMessage());
    e.printStackTrace();
  } catch (IOException e) {
    e.printStackTrace();
  }
  return context;
}
 
// anonyme Verschlüsselungssuiten hinzufügen
  public void setupCipher(SSLServerSocket socket) {
    String [] supported = socket.getSupportedCipherSuites();
    String [] anonCipherSuitesSupported = new String [supported.length];
    int numAnonCipherSuitesSupported = 0;
    for (int i = 0; i < supported.length; i++) {
      if(supported[i].indexOf("_anon_") > 0 ) {
        anonCipherSuitesSupported[numAnonCipherSuitesSupported++] = supported[i];
      }
    }
    String [] oldEnabled = socket.getEnabledCipherSuites();
    String [] newEnabled = new String[oldEnabled.length + numAnonCipherSuitesSupported];
    System.arraycopy(oldEnabled, 0, newEnabled, 0, oldEnabled.length);
    System.arraycopy(anonCipherSuitesSupported, 0, newEnabled, oldEnabled.length, numAnonCipherSuitesSupported);
    socket.setEnabledCipherSuites(newEnabled);
  }
(...)

Darüber hinaus braucht der Client einen Socket, der mit dem SSLServerSocket kompatibel ist. In der connect-Methode der Klasse ClientConnection (siehe Listing 5) ersetzt ihr zunächst den Client-Socket durch die folgenden Zeilen:

SSLContext context = setupSSLContext();
SSLSocketFactory socketFactory = context.getSocketFactory();
SSLSocket socket = (SSLSocket) socketFactory.createSocket(host, port);

Außerdem stellt ihr beim Client ein, welche Zertifikate akzeptiert werden. So erlaubt die selbstdefinierte Methode setupSSLContext() alle eingehenden Zertifikate (Listing 8) [17].

// alle Zertifikate akzeptieren
public SSLContext setupSSLContext() {
  SSLContext sc = null;
  TrustManager[] trustAllCerts = new TrustManager[] { new X509TrustManager() {
    public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException {
      // TODO Auto-generated method stub
    }
    public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException {
      // TODO Auto-generated method stub
    }
 
    public X509Certificate[] getAcceptedIssuers() {
      // TODO Auto-generated method stub
      return null;
    }
} };
 
// Trust-Manager installieren
try {
  sc = SSLContext.getInstance("SSL");
  sc.init(null, trustAllCerts, new java.security.SecureRandom());
} catch (NoSuchAlgorithmException e) {
  e.printStackTrace();
} catch (KeyManagementException e) {
  e.printStackTrace();
}
return sc;

Dank der eingerichteten Verschlüsselung kann der Server die verschlüsselte Kreditkarteninformation entschlüsseln und weiterverarbeiten (Abb. 3).

Abb. 3: Ausgabe von verschlüsselten Daten in lesbarer Form

XML-Signaturen

Daten, die in XML-Dateien exportiert und über das Internet versendet werden, sind ebenfalls von Missbrauch bedroht. Um ihre Integrität zu gewährleisten, könnt ihr den Empfehlungen des W3C folgen. Denn XML-Signaturen eignen sich zur Sicherung von Daten jeglicher Art. Hierfür stellt das Java API das Paket java.xml.crypto zur Verfügung. Damit unterstützt es die Erzeugung und Validierung von XML-Signaturen gemäß den empfohlenen Richtlinien.

Stay tuned

Regelmäßig News zur Konferenz und der Java-Community erhalten

 

In der Praxis lässt sich ein XML-Dokument wie in Listing 9 implementieren [18]. Hierbei wird der Klasse MySignature sowohl der Pfad des unsignierten als auch der des neuen XML-Dokuments übergeben. Die selbstdefinierte Methode createSignature() signiert das XML-Dokument in drei Schritten [19]:

  1. Schlüsselpaar bestehend aus privatem und öffentlichem Schlüssel generieren

  2. XML-Dokument importieren (siehe MySignature.importXML())

  3. Original-XML-Dokument mittels des Schlüsselpaars signieren und ein weiteres XML-Dokument zusammen mit der digitalen Signatur erzeugen

Mittels der Klasse aus Listing 9 lässt sich ein bestehendes XML-Dokument wie folgt signieren:

import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.security.InvalidAlgorithmParameterException;
import java.security.KeyException;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.NoSuchAlgorithmException;
import java.util.Collections;
import java.util.Iterator;
 
import javax.xml.crypto.MarshalException;
import javax.xml.crypto.dsig.CanonicalizationMethod;
import javax.xml.crypto.dsig.DigestMethod;
import javax.xml.crypto.dsig.Reference;
import javax.xml.crypto.dsig.SignatureMethod;
import javax.xml.crypto.dsig.SignedInfo;
import javax.xml.crypto.dsig.Transform;
import javax.xml.crypto.dsig.XMLSignature;
import javax.xml.crypto.dsig.XMLSignatureException;
import javax.xml.crypto.dsig.XMLSignatureFactory;
import javax.xml.crypto.dsig.dom.DOMSignContext;
import javax.xml.crypto.dsig.dom.DOMValidateContext;
import javax.xml.crypto.dsig.keyinfo.KeyInfo;
import javax.xml.crypto.dsig.keyinfo.KeyInfoFactory;
import javax.xml.crypto.dsig.keyinfo.KeyValue;
import javax.xml.crypto.dsig.spec.C14NMethodParameterSpec;
import javax.xml.crypto.dsig.spec.TransformParameterSpec;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerConfigurationException;
import javax.xml.transform.TransformerException;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;
 
import org.w3c.dom.Document;
import org.w3c.dom.NodeList;
import org.xml.sax.SAXException;
 
public class MySignature {
  private String path = "";
  private String output = "";
  public MySignature(String p,String dest) {
    this.path = p;
    this.output = dest;
  }
  public void createSignature() {
    XMLSignatureFactory fac = XMLSignatureFactory.getInstance("DOM");
    try {
      Reference ref = fac.newReference("", fac.newDigestMethod(DigestMethod.SHA384, null), Collections.singletonList (fac.newTransform (Transform.ENVELOPED, (TransformParameterSpec) null)),null, null);
      // SignedInfo-Element erstellen
      SignedInfo si = fac.newSignedInfo(fac.newCanonicalizationMethod(Canoni-calizationMethod.INCLUSIVE, (C14NMethodParameterSpec) null), fac.newSignatureMethod(SignatureMethod.RSA_SHA384, null), Collections.singletonList(ref));
        KeyPairGenerator kpg = KeyPairGenerator.getInstance("RSA");
        kpg.initialize(2048);
        KeyPair kp = kpg.generateKeyPair();
        // KeyValue mit DSA-PublicKey erstellen
        KeyInfoFactory kif = fac.getKeyInfoFactory();
        KeyValue kv = kif.newKeyValue(kp.getPublic());
        KeyInfo ki = kif.newKeyInfo(Collections.singletonList(kv));
        // XML-Dokument importieren
        Document doc = importXML(path);
        // DOMSignContext erstellen und den DSA-PrivateKey
        // sowie den Ort des Eltern-Elements angeben
        DOMSignContext dsc = new DOMSignContext(kp.getPrivate(), doc.getDocumentElement());
        // XMLSignatur-Objekt anlegen
        XMLSignature signature = fac.newXMLSignature(si, ki);
        // Kuvertierte Signatur zusammenstellen und generieren
        signature.sign(dsc);
        // neues Dokument abspeichern
        OutputStream os = new FileOutputStream(this.output);
        TransformerFactory tf = TransformerFactory.newInstance();
        Transformer trans = tf.newTransformer();
        trans.transform(new DOMSource(doc), new StreamResult(os));
    } catch (..)
    // TODO: Ausnahmen hinzufügen
  }
}
// XML-Signatur überprüfen
public boolean isXmlDigitalSignatureValid() throws Exception {
  boolean validFlag = false;
  Document doc = importXML(this.output);
  NodeList nl = doc.getElementsByTagNameNS(XMLSignature.XMLNS, "Signature");
  if (nl.getLength() == 0) {  
    throw new Exception("No XML Digital Signature Found, document is discarded");  
    }
    // DOMValidateContext-Objekt anlegen und einen KeyValue-KeySelector
    // sowie Kontext des Dokuments angeben
    DOMValidateContext valContext = new DOMValidateContext(new KeyValueKeySelector(), nl.item(0));  
    XMLSignatureFactory fac = XMLSignatureFactory.getInstance("DOM");  
    XMLSignature signature = fac.unmarshalXMLSignature(valContext);  
    validFlag = signature.validate(valContext);  
    if (validFlag == false) {
      System.err.println("Signature failed core validation");
      boolean sv = signature.getSignatureValue().validate(valContext);
      System.out.println("signature validation status: " + sv);
      // den Validierungsstatus jeder Referenz prüfen
      Iterator i = signature.getSignedInfo().getReferences().iterator();
      for (int j=0; i.hasNext(); j++) {
        boolean refValid = ((Reference) i.next()).validate(valContext);
        System.out.println("ref["+j+"] validity status: " + refValid);
      }
    } else {
      System.out.println("Signature passed core validation");
    }
    
    return validFlag;
    }
 
    // XML-Dokument importieren
    public Document importXML(String sPath) {
      DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
      dbf.setNamespaceAware(true);
      Document doc = null;
      try {
        doc = dbf.newDocumentBuilder().parse(new FileInputStream(sPath));
      } catch (SAXException | IOException | ParserConfigurationException e) {
      e.printStackTrace();
    }
    return doc;
  }
}
String path = "files/music-jaxb-en.xml";
String signedXML = "files/envelopedSignature.xml";
MySignature xmlSecurity = new MySignature(path,signedXML);
xmlSecurity.createSignature();

Abbildung 4 zeigt das signierte XML-Dokument, bei dem die digitale Signatur innerhalb des Wurzeltags eingefügt wird. Der hierfür verwendete Algorithmus, genannt RSA-SHA384, orientiert sich dabei an den Vorgaben von W3C.

Abb. 4: Signiertes XML-Dokument

Darüber hinaus lässt sich die Signatur des XML-Dokuments auf Richtigkeit hin validieren. Die selbst definierte Methode aus Listing 9, genannt isXmlDigitalSignatureValid(), lädt zunächst den Inhalt des Signature-Tags und übergibt diesen zusammen mit dem KeyValueKeySelector-Objekt (Listing 10) dem DOMValidateContext. Dann werden die eingelesenen XML-Knoten in Objekte umgewandelt und als XMLSignature-Objekt gespeichert. Das XMLSignature-Objekt bekommt anschließend das zuvor erstellte DOMValidateContext-Objekt übergeben. Die validate-Methode gibt true zurück, falls die Signatur laut den Regeln des W3C erfolgreich validiert worden ist; ansonsten gibt sie false zurück.

In Listing 10 ist der KeySelector definiert, der den öffentlichen Schlüssel aus dem KeyValue-Element abruft und ihn zurückgibt. Wenn der Schlüsselalgorithmus nicht mit dem Signaturalgorithmus übereinstimmt, wird der öffentliche Schlüssel ignoriert.

import java.security.KeyException;
import java.security.PublicKey;
import java.util.List;
import javax.xml.crypto.AlgorithmMethod;
import javax.xml.crypto.KeySelector;
import javax.xml.crypto.KeySelectorException;
import javax.xml.crypto.KeySelectorResult;
import javax.xml.crypto.XMLCryptoContext;
import javax.xml.crypto.XMLStructure;
import javax.xml.crypto.dsig.SignatureMethod;
import javax.xml.crypto.dsig.keyinfo.KeyInfo;
import javax.xml.crypto.dsig.keyinfo.KeyValue;
 
public class KeyValueKeySelector extends KeySelector {
 
  public KeySelectorResult select(KeyInfo keyInfo,KeySelector.Purpose purpose,AlgorithmMethod method,XMLCryptoContext context)throws KeySelectorException {
 
    if (keyInfo == null) {
      throw new KeySelectorException("Null KeyInfo object!");
    }
    SignatureMethod sm = (SignatureMethod) method;
    List list = keyInfo.getContent();
 
    for (int i = 0; i < list.size(); i++) {
      XMLStructure xmlStructure = (XMLStructure) list.get(i);
      if (xmlStructure instanceof KeyValue) {
        PublicKey pk = null;
        try {
          pk = ((KeyValue)xmlStructure).getPublicKey();
        } catch (KeyException ke) {
          throw new KeySelectorException(ke);
        }
        // prüfen, ob der Algorithmus kompatibel mit der Methode ist
        if (algEquals(sm.getAlgorithm(), pk.getAlgorithm())) {
          return new SimpleKeySelectorResult(pk);
        }
      }
    }
    throw new KeySelectorException("No KeyValue element found!");
  }
 
  private static boolean algEquals(String algURI, String algName) {
    if (algName.equalsIgnoreCase("DSA") && algURI.equalsIgnoreCase("http://www.w3.org/2009/xmldsig11#dsa-sha256")) {
      return true;
    } else if (algName.equalsIgnoreCase("RSA") && algURI.equalsIgnoreCase("http://www.w3.org/2001/04/xmldsig-more#rsa-sha384")) {
      return true;
    } else {
      return false;
    }
  }
}

Das in Listing 10 angelegte KeyValueKeySelector-Objekt wird benötigt, um Schlüssel für die Validierung der digitalen Signatur zu finden. Alternativ kann direkt ein öffentlicher Schlüssel angegeben werden. Allerdings ist es oft nicht vorhersehbar, welcher Schlüssel passt.

 

Der KeyValueKeySelector (Listing 10) leitet die abstrakte Klasse KeySelector ab. Er versucht, einen angemessenen öffentlichen Schlüssel zu ermitteln, indem er auf Daten der KeyValue-Elemente aus dem KeyInfo-Tag eines XMLSignature-Objekts zurückgreift. Diese Implementierung überprüft nicht, ob der Schlüssel verlässlich ist. Sobald der KeyValueKeySelector einen geeigneten öffentlichen Schlüssel findet, wird dieser über das SimpleKeySelectorResult-Objekt zurückgegeben (Listing 11).

import java.security.Key;
import java.security.PublicKey;
 
import javax.xml.crypto.KeySelectorResult;
 
public class SimpleKeySelectorResult implements KeySelectorResult {
  private PublicKey pk;
  
  public SimpleKeySelectorResult(PublicKey pk) {
    this.pk = pk;
  }
  
  public Key getKey() {
    return pk;
  }
  
}

In Listing 12 könnt ihr sehen, wie sich die selbst definierte Klasse MySignature initialisieren und ausführen lässt.

String path = "files/music-jaxb-en.xml";
String signedXML = "files/envelopedSignature.xml";
MySignature xmlSecurity = new MySignature(path,signedXML);
xmlSecurity.createSignature();
try {
  xmlSecurity.isXmlDigitalSignatureValid();
} catch (Exception e) {
  e.printStackTrace();
}

 

Links & Literatur

[1] Gollmann, D.: „Computer Security“; Wiley, 2011

[2] Marktgröße: https://www.fortunebusinessinsights.com/industry-reports/cyber-security-market-101165

[3] Gehälter: https://mondo.com/blog-highest-paid-cybersecurity-jobs/

[4] Java SE: https://docs.oracle.com/javase/9/security/toc.htm

[5] Basics of Java Security: https://www.baeldung.com/java-security-overview

[6] CallbackHandler: https://docs.oracle.com/javase/7/docs/api/javax/security/auth/callback/CallbackHandler.html

[7] webMethods LoginModule: https://documentation.softwareag.com/webmethods/wmsuites/wmsuite9-7/Security_Infrastructure/9-7_Security_Infrastructure/using/modules.htm

[8] webMethods Integration: https://tech.forums.softwareag.com/pub/webmethods-integration-free-trial-download-thank-you

[9] Apache Shiro: https://shiro.apache.org

[10] JCA: https://docs.oracle.com/javase/9/security/java-cryptography-architecture-jca-reference-guide.htm

[11] Message Digest: https://docs.racket-lang.org/crypto/digest.html

[12] Triple DES unter Java: https://stackoverflow.com/questions/20227/how-do-i-use-3des-encryption-decryption-in-java

[13] Secure Socket: https://www.ibm.com/docs/en/i/7.2?topic=jsse-changing-your-java-code-use-secure-sockets-layer

[14] Tcpflow: https://www.technomancy.org/networking/tcpflow-howto-see-network-traffic/

[15] Harold, E. R.: „Java Network Programming“; O’Reilly, 2014

[16] Keystore, Truststore, Zertifikate: https://unix.stackexchange.com/questions/347116/how-to-create-keystore-and-truststore-using-self-signed-certificate

[17] SSLContext: http://www.java2s.com/example/java/security/trust-all-certificate.html

[18] XML-Signatur: https://docs.oracle.com/javase/9/security/java-xml-digital-signature-api-overview-and-tutorial.htm

[19] XML-Signatur-Tutorial: https://www.c-sharpcorner.com/UploadFile/5fd9bd/xml-digital-signature-in-java/

The post Java-Anwendungen mit Bordmitteln absichern appeared first on JAX.

]]>
Einfacher als gedacht https://jax.de/blog/einfacher-als-gedacht/ Tue, 24 Aug 2021 07:06:56 +0000 https://jax.de/?p=83973 In der Regel bestehen Microservices-Projekte aus mehreren einzelnen Services, die als getrennte Deployment-Einheiten separat betrieben werden und sich dabei gegenseitig aufrufen. Für die Security des Gesamtsystems ergeben sich hieraus mehrere Konsequenzen. Zum einen muss jeder einzelne Microservice für sich gewisse Securityrichtlinien beachten. Doch das allein reicht noch nicht aus, da auch die Kommunikation mit den anderen Services abgesichert werden muss.

The post Einfacher als gedacht appeared first on JAX.

]]>
Erst durch beide Maßnahmen erreicht das Geflecht an Services einen Securitystandard, der bei einem monolithischen System sehr viel einfacher zu erreichen ist. Der administrative Umgang mit den Zertifikaten für die Transport Layer Security (TLS) kann bei einem verteilten System einen recht erheblichen Aufwand verursachen.

Allein schon durch die hohe Anzahl an eigenständigen Services erhöht sich die Wahrscheinlichkeit einer Sicherheitslücke. Sobald neue sogenannte Common Vulnerabilities and Exposures (CVEs) veröffentlicht werden, müssen diese unter Umständen in mehreren Services zeitnah ausgebessert werden. Erst wenn alle Microservices anschließend neu deployt wurden, gilt dieses Sicherheitsrisiko als behoben. Ein möglicher Angreifer hat im Grunde die Qual der Wahl, welchen der vielen Services er als Erstes zu kompromittieren versuchen soll. Sollte der Angriffsversuch auf den ersten Microservice nicht zum Erfolg führen, hat er noch genügend andere Opfer, über die er in das Geflecht der Microservices eindringen kann. Werden die Services über eine ungesicherte Verbindung aufgerufen, so ergeben sich für den Hacker noch mehr Möglichkeiten, in das Gesamtsystem einzubrechen.

Zero Trust

Werden die zuvor genannten Securityprobleme bei Microservices konsequent zu Ende gedacht, kommt man im Grunde zu dem Ergebnis, dass nur ein Zero-Trust-Ansatz einen ausreichenden Sicherheitsschutz bieten kann.

Zero Trust besagt, dass im Grunde keinem (Micro-)Service vertraut werden darf, sogar dann nicht, wenn er sich in einer sogenannten Trusted Zone befindet. Jeder Request zwischen den Services muss authentifiziert (AuthN) und autorisiert (AuthZ) werden und mittels TLS abgesichert sein. Als Authentifikationsmerkmal wird oft ein JSON Web Token (JWT) verwendet, das bei jedem Request mitgeschickt werden muss und somit den Aufrufer identifiziert. Dieses JWT wird entweder End-to-End verwendet, d. h., dass es während der gesamten Aufrufkette nicht ausgetauscht wird, oder man generiert sich mit einem TokenExchangeService für jeden einzelnen Request ein eigenes neues Token. Zu guter Letzt soll die Absicherung nicht nur auf der HTTP- oder gRPC-Ebene stattfinden (OSI Layer 7 Application), sondern auch in den darunter liegenden Netzwerkschichten Transport und Network (OSI Layer 4 und 3). Damit wird dem OWASP-Securityprinzip [1] Defense in Depth genüge getan.

Nicht jeder sicherheitsverantwortliche Mitarbeiter will diesen finalen Schritt gehen, da klar erkennbar ist, dass die Umsetzung von Zero Trust nicht gerade trivial ist. In der Gegenüberstellung Aufwand gegen Nutzen wird dabei oft der (falsche) Schluss gezogen, dass für Zero Trust der Aufwand viel zu hoch wäre, obwohl einem die innere Securitystimme sagt, dass Zero Trust der richtige Ansatz ist. Mittlerweile gibt es jedoch Systeme, die den Aufwand für Zero Trust sehr stark minimieren, womit der Nutzen die Oberhand gewinnt.

Verschaffen Sie sich den Zugang zur Java-Welt mit unserem kostenlosen Newsletter!

Zero Trust bei Microservices

Als Konsequenz der vorherigen Überlegungen müssen für Zero Trust bei Microservices mehrere Funktionalitäten umgesetzt werden. Jeder Request muss Informationen über den Aufrufer enthalten. Am einfachsten gelingt das mit einem JWT, das im HTTP-Header Authorization als Typ Bearer mitgeschickt wird. Der aufgerufene Service validiert dieses JWT und führt dann die notwendigen Berechtigungsprüfungen durch. Bei Verwendung eines Service-Mesh-Tools kann diese Prüfung auch von einem Sidecar übernommen oder ergänzt werden (dazu mehr in den folgenden Abschnitten). Zur Absicherung der Kommunikation mit TLS sind SSL-Zertifikate notwendig, die die Eigenschaft haben, dass sie nach einem gewissen Zeitintervall ungültig werden und somit ausgetauscht werden müssen. Das kann wegen der hohen Anzahl an Microservices nicht manuell erfolgen. Für diese administrative Aufgabe bieten Service-Mesh-Tools eine passende Automatisierung an, wodurch der Aufwand für die Ops-Kollegen gegen Null geht.

Um jetzt noch dem OWASP-Securityprinzip Defense in Depth gerecht zu werden, sollten die Kommunikationsverbindungen zwischen den Microservices mit Firewallregeln berechtigt oder unterbunden werden. Somit erfolgt eine weitere Absicherung der Kommunikation auf TCP/IP-Ebene. Kubernetes sieht dafür das Konzept der Network Policies vor. Auch hierzu mehr in den kommenden Abschnitten.

Typischer Ausgangspunkt

Die meisten Microservices-Projekte, die auf Kubernetes betrieben werden, starten mit der Ausgangssituation in Abbildung 1.

Abb. 1: Low Secure Deployment

Für die Ingress-Kommunikation stellen die Cloud-Betreiber passende Komponenten zur Verfügung oder beschreiben mit Tutorials, wie diese Komponenten einfach installiert werden können. Der Kommunikationspfad aus dem Internet geht meist über eine Firewall und wird mit einem LoadBalancer in Richtung Kubernetes-Cluster geroutet. Dafür stellen die Cloud-Provider oft fertige Lösungen zur Verfügung, die auch sehr einfach auf TLS umgestellt werden können.

Jetzt beginnt der Teil, bei dem man eigenverantwortlich weitere Maßnahmen zur Sicherheit umsetzen muss. Ein Ingress-Controller innerhalb Kubernetes kümmert sich dann um die Weiterleitung des Requests innerhalb des Clusters. Dieser wird typischerweise als SSL-Endpunkt mit einem firmeneigenen Zertifikat ausgestattet. Als Folge davon werden die Requests nach der SSL-Terminierung ohne TLS an die jeweiligen Microservices weitergeleitet, d. h., die Kommunikation innerhalb des Clusters erfolgt komplett ohne Absicherung. Manche der Microservices (oder doch schon alle?) besitzen eine Securityprogrammierung, die das empfangene JWT validiert und anschließend mit den enthaltenen Claim-Werten die Berechtigungsprüfung durchführt. Zur Validierung des JWT müssen (sporadisch) Requests zum Identity Provider (IDP), der das JWT ausgestellt hat, abgeschickt werden. Das erfolgt in der Regel mit TLS, da der IDP nur einen HTTPS-Zugang anbietet. Die interne Aufwand-Nutzen-Analyse hat ergeben, dass mit den vorhandenen Mitteln bei vertretbarem Aufwand ein gewisser Grad an Security erreicht worden ist. Das Projektteam ist sich bewusst, dass dieses Setting keinem Zero-Trust-Ansatz entspricht, glaubt aber, dass der Aufwand für Zero Trust viel zu hoch ist. Mangels besseren Wissens gibt man sich mit diesem (geringen) Level an Security zufrieden.

Service Mesh

Ein sehr viel höheres Securitylevel kann man mit Service-Mesh-Tools wie Istio oder Linkerd (u. v. m.) erreichen. Diese Tools bieten hierfür entsprechende Funktionalitäten an, die zum Beispiel das Zertifikatsmanagement automatisieren. Am Beispiel von Istio sollen diese Features genauer betrachtet werden.

Mutual TLS

Jeder Service, der Bestandteil eines Service Mesh wird, bekommt ein sogenanntes Sidecar, das die eingehende und ausgehende Kommunikation zum Service steuert und überwacht. Gesteuert wird das Verhalten des Sidecar über eine zentrale Steuerungskomponente, die dem Sidecar die passenden Informationen und Anweisungen übermittelt. Beim Start des Pods, der aus dem Service und dem Sidecar besteht, holt sich das Sidecar ein individuelles SSL-Zertifikat von der zentralen Steuereinheit ab. Damit ist das Sidecar in der Lage, eine Mutual-TLS-Verbindung (mTLS) zu etablieren. Nur der Request vom Sidecar zum Service, also die Kommunikation innerhalb des Pods, erfolgt dann ohne TLS. Nach einem vordefinierten Zeitintervall (bei Istio ist der Default 24 h), lässt sich das Sidecar automatisch ein neues Zertifikat vom Steuerungsservice ausstellen. Dieses neue Zertifikat wird dann für die nächsten 24 Stunden für die mTLS-Verbindung verwendet. Mit der in Listing 1 gezeigten Istio-Regel wird eine mTLS-Kommunikation für das gesamte Service Mesh verpflichtend.

apiVersion: security.istio.io/v1beta1
kind: PeerAuthentication
metadata:
  name: default
  namespace: istio-system
spec:
  mtls:
    mode: STRICT

Für eine Übergangslösung, in der noch nicht alle Services in den Service Mesh integriert wurden, gibt es einen speziellen Modus: PERMISSIVE. Damit werden Services innerhalb des Service Mesh mit mTLS angesprochen und Services, die außerhalb des Service Mesh laufen, werden weiterhin ohne TLS aufgerufen. Auch das erfolgt automatisch und wird vom jeweiligen Sidecar, das den Aufruf initiiert, entsprechend ausgeführt.

Der Vorteil für die Security liegt hier auf der Hand. Die Kommunikation innerhalb des Service Mesh erfolgt über mTLS, und das Zertifikatsmanagement läuft in kurzen Zeitintervallen vollautomatisch ab. Darüber hinaus erfolgt das gesamte mTLS-Handling ohne einen Eingriff in den Code des Service, läuft also aus Sicht des Service völlig transparent ab. Diese Absicherung auf dem Application Layer des Netzwerkstacks kann dann ohne Probleme mit Absicherungen auf Layer 3 und 4 kombiniert werden (siehe kommende Abschnitte).

Ingress Gateway

Istio bietet auch ein sogenanntes Ingress Gateway an, das den Eintrittspunkt in den Kubernetes-Cluster und somit in das Service Mesh regelt. Es kann damit den Ingress Controller aus der vorherigen Systemlandschaft (Abb. 1) vollständig ersetzen.

Man könnte Istio auch ohne Ingress Gateway betreiben, würde dann aber eine Menge an Funktionalitäten verlieren. Der Vorteil des Ingress Gateway liegt darin, dass dort schon die gesamten Istio-Regeln greifen. Somit sind alle Regeln für Trafficrouting, Security, Releasing usw. anwendbar. Auch die Kommunikation vom Ingress Gateway zum ersten Service ist bereits mit mTLS abgesichert. Im Falle eines alternativen Ingress Controllers wäre dieser Request noch ohne SSL-Absicherung. Die Bezeichnung Gateway (anstatt Controller) wurde von Istio ganz bewusst gewählt, da mit dem existierenden Funktionsumfang das Ingress Gateway auch als sogenanntes API Gateway betrieben werden kann.

Für den gesicherten Eintritt in das Cluster kann das Ingress Gateway mit dem entsprechenden Firmenzertifikat konfiguriert werden, ganz analog zum Vorgehen bei einem Ingress Controller. Die Istio-Regel in Listing 2 definiert die Funktionsweise des Ingress Gateways.

apiVersion: networking.istio.io/v1alpha3
  kind: Gateway
  metadata:
    name: mygateway
  spec:
    selector:
      istio: ingressgateway
    servers:
    - port:
        number: 443
        name: https
        protocol: HTTPS
      tls:
        mode: SIMPLE
        credentialName: mytls-credential
      hosts:
      - myapp.mycompany.de
Verschaffen Sie sich den Zugang zur Java-Welt mit unserem kostenlosen Newsletter!

 

Es wird ein SSL-Port (443) für alle Requests auf den Hostnamen myapp.mycompany.de geöffnet und das zugehörige SSL-Zertifikat wird aus dem Kubernetes Secret mytls-credential ausgelesen. Die Erstellung des SSL-Secrets erfolgt mit der Kubernetes-Regel in Listing 3.

apiVersion: v1
   kind: Secret
   metadata:
     name: mytls-credential
   type: kubernetes.io/tls
   data:
     tls.crt: |
           XYZ...
     tls.key: |
           ABc...

Damit ist auch hier der Eingang in den Cluster mit TLS abgesichert und die weiterführende Kommunikation erfolgt mit dem mTLS-Setting von Istio.

Network Policy

Nachdem die HTTP-Ebene mit SSL abgesichert ist, sollten nun noch weitere Netzwerkschichten (OSI Layer 3 und 4) abgesichert werden. Um in Kubernetes so etwas wie Firewalls zu etablieren, gibt es das Konzept der Network Policy. Diese Policies definieren die Netzwerkverbindungen zwischen den Pods, wobei die Einhaltung der Regeln von einem zuvor installierten Netzwerk-Plug-in durchgesetzt werden. Network Policies ohne ein solches Netzwerk-Plug-in haben keinen Effekt. Kubernetes bietet eine große Auswahl an Plug-ins [2] an, die man in einem Kubernetes-Cluster installieren kann.

Als Best Practice gilt es, eine sogenannte Deny-All-Regel zu definieren. Damit wird im gesamten Cluster die Netzwerkkommunikation zwischen den Pods unterbunden. Die Deny-All-Ingress-Regel verbietet jede eingehende Kommunikation auf Pods im zugehörigen Namespace (Listing 4).

apiVersion: networking.k8s.io/v1
  kind: NetworkPolicy
  metadata:
    name: default-deny-ingress
    namespace: my-namespace
  spec:
    podSelector: {}
    policyTypes:
    - Ingress

Nachdem diese Regel aktiviert wurde, kann man nun gezielt die einzelnen Ingress-Verbindungen freischalten. Die Regel in Listing 5 gibt beispielsweise den Ingress-Traffic auf den Pod mit dem Label app=myapp frei, aber nur wenn der Request vom Ingress Gateway kommt.

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: access-myapp
  namespace: my-namespace
spec:
  podSelector:
    matchLabels:
      app: myapp
  ingress:
  - from:
    - podSelector:
        matchLabels:
          istio: ingressgateway

Für jede weitere zulässige Verbindung muss die Regel entsprechend erweitert oder es müssen zusätzliche Regeln definiert werden. Durch die Etablierung der zuvor genannten Regeln hat sich das Anfangs-Deployment (Abb. 1) verändert, wie in Abbildung 2 gezeigt wird.

Abb. 2: Medium Secure Deployment

Die SSL-Terminierung wird nun vom Istio Ingress Gateway ausgeführt. Jeder weitergeleitete Request wird mit mTLS abgesichert. Zusätzlich definiert eine Network Policy je Pod den gewollten Ingress Request. Alle ungewollten Requests werden vom Network-Plug-in unterbunden. Damit ist schon mal ein großer Schritt in Richtung Zero Trust umgesetzt. Was jetzt noch fehlt, sind Berechtigungsprüfungen, die die Zulässigkeit der Aufrufe noch weiter eingrenzen.

Authentication (AuthN) und Authorization (AuthZ)

Für AuthN und AuthZ bietet Istio eine Menge an Regeln, mit denen man sehr granular steuern kann, welche Aufrufe berechtigt sind und welche nicht. Diese Regeln werden von den Sidecars und vom Ingress Gateway beachtet, wodurch der gesamte definierte Regelsatz überall im Service Mesh angewandt wird. Auch hiervon merkt die jeweilige Applikation nichts, da dies transparent von den jeweiligen Sidecars übernommen wird.

Die Authentifizierung erfolgt auf Basis eines JSON Web Tokens, das von Istio überprüft wird. Dazu werden die notwendigen JWT-Validierungen ausgeführt, wobei auf den ausstellenden Identity Provider (IDP) zugegriffen wird. Nach erfolgreicher Prüfung gilt der Request innerhalb des gesamten Service Mesh als authentifiziert. Am besten geschieht das im Ingress Gateway, womit die Prüfung gleich beim Eintritt in das Service Mesh bzw. Cluster ausgeführt wird.

Mit der Regel in Listing 6 wird Istio angewiesen, das empfangene JWT gegen den IDP mit dem URL unter [3] zu validieren.

apiVersion: security.istio.io/v1beta1
kind: RequestAuthentication
metadata:
  name: ingress-idp
  namespace: istio-system
spec:
selector:
    matchLabels:
      istio: ingressgateway
  jwtRules:
  - issuer: "my-issuer"
    jwksUri: https://idp.mycompany.de/.well-known/jwks.json

Wie aus der Regel ersichtlich wird, sind die Issuer als Array zu definieren, d. h. es können auch mehrere unterschiedliche IDPs angegeben werden.

Damit gilt der Request zwar als authentifiziert, es finden aber noch keine Berechtigungsprüfungen statt. Diese müssen separat mit einem anderen Regeltyp angegeben werden. Ebenso wie bei der Network Policy gibt es auch hier eine Best Practice, mit der alle Zugriffe innerhalb des Service Mesh als nicht berechtigt deklariert werden. Dies kann mit der Regel in Listing 7 festgelegt werden.

apiVersion: security.istio.io/v1beta1
kind: AuthorizationPolicy
metadata:
  name: allow-nothing
  namespace: istio-system
spec:
  {}

Jetzt kann wie zuvor bei der Network Policy jeder einzelne Zugriff auf einen genau spezifizierten Pod sehr feingranular geregelt werden. Innerhalb der Regel kann in den Abschnitten from, to und when definiert werden, woher der Request kommen muss, welche HTTP-Methoden und Endpunkte aufgerufen werden sollen und welche Authentifizierungsinhalte (Claims im JWT) enthalten sein müssen. Erst wenn alle diese Kriterien zutreffen, wird der Zugriff erlaubt (action: allow), wie in Listing 8 gezeigt wird.

apiVersion: security.istio.io/v1beta1
kind: AuthorizationPolicy
metadata:
name: my-app
  namespace: my-namespace
spec:
  selector:
    matchLabels:
      app: my-app
  action: ALLOW
  rules:
  - from:
    - source:
        principals: ["cluster.local/ns/ns-xyz/sa/my-partner-app"]
- source:
        namespaces: ["ns-abc", "ns-def"]
    to:
    - operation:
        methods: ["GET"]
        paths: ["/info*"]
    - operation:
        methods: ["POST"]
        paths: ["/data"]
    when:
    - key: request.auth.claims[iss]
      values: ["https://idp.my-company.de"]

Neben der Möglichkeit, einen Request mit der action: allow zu berechtigen, gibt es noch die Möglichkeit, gewisse Request zu verbieten (action: deny) oder mit action: custom eigene Berechtigungsprüfungen in den Service Mesh zu integrieren. Damit ist es möglich schon vorhandene Berechtigungssysteme, wie sie in vielen Unternehmen existieren, weiterhin zu nutzen.

Verschaffen Sie sich den Zugang zur Java-Welt mit unserem kostenlosen Newsletter!

 

Finales System

Nach Anwendung der gesamten Regeln ergibt sich die Zero-Trust-Infrastruktur in Abbildung 3.

Abb. 3: Secure Deployment

Neben der mit TLS abgesicherten Kommunikation sind jetzt noch Berechtigungsprüfungen hinzugekommen, die im Grunde in jedem der Sidecars ausgewertet werden. Als Basis für die Authentifizierung dient das im HTTP-Header mitgeschickte JWT. Um diesen finalen Zustand zu erreichen, sind die in Tabelle 1 gezeigten Regeln notwendig.

Funktion Regeln
TLS Terminierung Gateway-Regel und Kubernetes Secret
mTLS Network Policy (Deny Ingress) und jeweils eine Network Policy pro Pod
Network Segmentation Request Authentication
Authentication Authorization Policy (Allow Nothing) und jeweils eine Authorization Policy pro Pod
Authorization Regeln

Tabelle 1: Übersicht Regeln

Insgesamt also nur sechs Basisregeln und pro Pod noch zwei weitere Regeln zur Zugriffssteuerung. Mit dieser geringen Anzahl an Regeln sollte nun die zuvor aufgestellte Aufwand- Nutzen-Analyse für eine Zero-Trust-Infrastruktur zu einem ganz anderen Ergebnis führen.

Fazit

Zugegeben, der Aufwand, ein Service-Mesh-Tool im Projekt zu etablieren, ist nicht gerade gering. Aber neben den ganzen Securityaspekten bieten diese Tools noch sehr viel mehr an Funktionalität, die einem beim Betrieb von Microservices einen sehr guten Dienst erweisen können. Rein aus dem Gesichtspunkt der Security wäre der Aufwand für ein Service-Mesh-Tool wohl relativ hoch, aber im Zusammenwirken mit den anderen Service-Mesh-Funktionalitäten wie Trafficrouting, Resilience und Releasing ergibt sich durchaus eine positive Bilanz in der Aufwand-Nutzen-Analyse. Außerdem wäre ein automatisiertes Zertifikatsmanagement auch nicht gerade trivial und bei einer selbst implementierten Lösung vielleicht auch nicht ganz fehlerfrei. Im Bereich der Security sollte man sich aber keine Fehler erlauben.

Die Kombination mit der Network Policy kann ohne Einflüsse auf Istio parallel etabliert werden, womit einem Defense-in-Depth-Ansatz entsprochen wird. Die Berechtigungsprüfungen können sehr fein gesteuert werden und lassen im Grunde keine Wünsche offen. Hier ist allerdings Vorsicht geboten, da durch die umfangreichen Möglichkeiten Situationen entstehen können, die sehr komplex und damit nur noch schwer verständlich sind. „Keep it simple, stupid“ (KISS) sollte hier als Handlungsoption immer wieder in Betracht gezogen werden.

Um auch bei der Berechtigungsprüfung einen Defense-in-Depth-Ansatz zu etablieren, sollte natürlich noch in den jeweiligen Applikationen eine Berechtigungsprüfung stattfinden. Im Bereich des Auditing wird von Istio derzeit nur Stackdriver unterstützt. Hier wäre eine größere Auswahl an Auditsystemen wünschenswert. Doch was noch nicht ist, kann ja noch werden.

Insgesamt lässt sich mit ein paar Kubernetes- bzw. Istio-Regeln eine Zero-Trust-Infrastruktur etablieren, die wohl bei jedem Securityaudit standhalten wird.

Links & Literatur

[1] https://github.com/OWASP/DevGuide/blob/master/02-Design/01-Principles%20of%20Security%20Engineering.md

[2] https://kubernetes.io/docs/concepts/cluster-administration/addons/

[3] https://idp.mycompany.de/.well-known/jwks.json

The post Einfacher als gedacht appeared first on JAX.

]]>