Performance & Security Blog - JAX-Konferenz https://jax.de/blog/performance-security/ Java, Architecture & Software Innovation Fri, 18 Oct 2024 13:00:12 +0000 de-DE hourly 1 https://wordpress.org/?v=6.5.2 Sichere Web-Apps für alle https://jax.de/blog/sichere-web-apps-content-security-policy-csp-gegen-cross-site-scripting-xss-tipps-tricks/ Fri, 05 Jan 2024 11:53:43 +0000 https://jax.de/?p=89358 Das Einschleusen von Schadcode über eine fremde Domäne lässt sich mit einer Content Security Policy (CSP) erheblich einschränken. Erfahren Sie, wie Sie Ihre Webapplikation gegen Cross-site-Scripting-(XSS-)Attacken härten können.

The post Sichere Web-Apps für alle appeared first on JAX.

]]>
Formulare in Webapplikationen sind für so manche Hackerattacke ein geeignetes Einfallstor. Ganz gleich, ob es ein Angriff auf die Datenbank mit einer SQL Injection ist, oder ob schädliches JavaScript per XSS [1] nachgeladen und zur Ausführung gebracht wird.

Gefahr durch Eingaben

Sicherlich ist der erste und wichtigste Schritt, sämtliche Variablen, die durch Formularfelder befüllt werden, auf eine gültige Eingabe hin zu prüfen. Ein gängiges Mittel ist die Validierung über reguläre Ausdrücke, die sicherstellen, dass nur gültige Zeichen und ein korrektes Format zugelassen werden. Möchte man aber beispielsweise formatierte und umfangreiche Artikel ermöglichen, wie es bei einem Content-Management-System der Fall ist, haben die gängigen Schutzmaßnahmen durchaus ihre Schwächen. Denn das Prinzip, möglichst die Eingabe zu beschränken und gefährliche Zeichen herauszufiltern, hat bei formatiertem Text seine Grenzen.

Wollen wir beispielsweise ein Zitat in Anführungszeichen setzen, können wir die beiden Zeichen ‘ und “ nicht einfach aus dem Text entfernen, da diese ja Bestandteil des Inhalts sind. In der Vergangenheit nutzte man bei dieser Problematik einen zusätzlichen Mechanismus, um Schutz vor Cyberangriffen sicherzustellen. Hierfür werden sämtliche kritische Zeichen wie spitze Klammern, Anführungszeichen und Backslashes über ihre HTML Escapes ersetzt. Damit gelten diese als problematisch klassifizierten Sonderzeichen nicht mehr als Quellcode, da aus > ein &gr; wird. Daher sind die soeben beschriebenen Maßnahmen besonders wirkmächtig und bieten bereits einen effektiven Schutz.

Die Schutzwirkung lässt sich allerdings über den Mechanismus CSP erheblich verbessern, da dieser weit über das Prüfen von Nutzereingaben hinausgeht. Sämtliche in einer Webapplikation geladene Ressourcen wie Bilder, CSS und JavaScript lassen sich dank dieser Technologie steuern.

Die Content Security Policy ist ein Projekt des W3C und greift als Bestandteil des Browsers – es sind keine zusätzlichen Plug-ins oder Ähnliches zu installieren. Für aktuelle Browser gilt CSP Level 3. Auf der entsprechenden Webseite des W3C [2] finden Sie eine vollständige Übersicht aller Versionen einschließlich der unterstützenden Webbrowser.

In Umgebungen, die erhöhte Sicherheitsanforderungen erfordern, beispielsweise Onlinebanking, kann die Browserversion des Clients abgefragt werden und bei erheblich veralteten Varianten des Browsers somit der Zugriff verweigert werden.

String userAgent = request.getHeader("user-agent");
String browserName = "";
String  browserVer = "";
if(userAgent.contains("Chrome")){
  String substring=userAgent.substring(userAgent.indexOf("Chrome")).split(" ")[0];
  browserName=substring.split("/")[0];
  browserVer=substring.split("/")[1];
} else if(userAgent.contains("Firefox")) {
  String substring=userAgent.substring(userAgent.indexOf("Firefox")).split(" ")[0];
  browserName=substring.split("/")[0];
  browserVer=substring.split("/")[1];
}

Stay tuned

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

 

Listing 1 zeigt die Möglichkeit, über den request die Identifikation des Browsers auszulesen. Anschließend demonstrieren die beiden Beispiele, wie die Versionsnummer aus dem Text für Chrome und Firefox extrahiert wird. Bei dieser Strategie sollte man sich aber bewusst sein, dass es für Schwindler recht einfache Möglichkeiten gibt, die Nutzung eines anderen Browsers vorzugaukeln.

Damit wird uns noch einmal vor Augen geführt, dass Web Application Security als ganzheitliches Konzept zu verstehen ist, in dem mehrere Maßnahmen als Verbund wirken – denn einzelne Techniken lassen sich durchaus umgehen. Schauen wir uns daher nun den Wirkmechanismus von CSP im Detail an.

Beginnen wir mit einer reinen HTML-Ausgabe, die CSS, Google Fonts, CDN JavaScript, ein YouTube iFrame und Grafiken enthält. Listing 2 dient uns als Ausgangspunkt, ohne den Einsatz von CSP.

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    
    <title>Content Security Policy Demo</title>
    
    <link rel="stylesheet" href="style.css" />
    
    <link
      href="https://fonts.googleapis.com/css2?family=Roboto:wght@400;700&display=swap"
      rel="stylesheet" />
    
    <link
      rel="stylesheet"
      href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css"
      integrity="sha384-TX8t27EcRE3e/ihU7zmQxVncDAy5uIKz4rEkgIXeMed4M0jlfIDPvg6uqKI2xXr2"
      crossorigin="anonymous" />
  </head>

  <body>
    <main>
      <div class="box">
        <div id="vue"></div>
      </div>

      <div class="box">
        <div class="embed">
          <iframe width="100%" height="500px" frameborder="0"
            src="https://www.youtube.com/embed/3nK6rcAbuzo"
            allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
            allowfullscreen></iframe>
        </div>
      </div>

      <div class="box">
        <div class="grid">
          <img src="https://images.unsplash.com/photo-1535089780340-34cc939f9996?ixlib=rb-1.2.1&auto=format&fit=crop&w=500&q=80" />
          <img src="https://images.unsplash.com/photo-1587081917197-95be3a2d2103?ixlib=rb-1.2.1&auto=format&fit=crop&w=500&q=80" />
          <img src="https://images.unsplash.com/photo-1502248103506-76afc15f5c45?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=500&q=80" />
        </div>
      </div>
    </main>

    <script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/vue.min.js"></script>
    <script nonce="rAnd0m">
      new Vue({
        el: '#vue',
        render(createElement) {
          return createElement('h1', 'Hello World!');
        },
      });
    </script>
  </body>
</html>

Abb. 1: HTML ohne CSP

Wie wir in Abbildung 1 sehen können, handelt es sich hierbei um eine einfache HTML-Seite, die verschiedenen Content lädt. Dazu gehört zum Beispiel auch, das JavaScript-Framework Bootstrap extern über das Content Delivery Network (CDN) jsDelivr zu laden. Wer mit dem Thema CDN noch nicht vertraut ist, findet dazu einen sehr informativen Artikel auf Wikipedia [3]. Stellen wir uns nun einmal ein realistisches Szenario aus der Praxis vor.

Es könnte nun sein, dass die von uns verwendete Bootstrap-Bibliothek eine bekannte Sicherheitslücke für XSS-Angriffe enthält. Der Content des CDN von jsDelivr ist durchaus vertrauenswürdig. Ein Angreifer könnte nun aber versuchen, von einem Webspace eine eigens präparierte JavaScript-Datei über die Bootstrap-Sicherheitslücke einzuschleusen. Ich möchte an dieser Stelle natürlich kein Hackertutorial an die Hand geben und erklären, wie ein solcher Angriff ausgeführt werden kann; ebensowenig möchte ich Ideen verbreiten, welcher Unfug sich über XSS anstellen lässt. Mir geht es darum, eine zuverlässige Methode vorzustellen, die den Angriff wirkungsvoll verhindern kann. Also richten wir unser Augenmerk nun auf die Prävention von XSS.

Schutz durch CSP

Mittels CSP werden nun Meta-Header-Attribute gesetzt, mit denen bestimmt werden kann, was zuverlässige Quellen sind. Das erlaubt es jedem Contenttyp (Ressource, also Bild, CSS und so weiter), explizite, vertrauenswürdige Quellen (Domains) zuzuweisen. Damit lassen sich JavaScript-Dateien so einschränken, dass sie nur vom eigenen Server geladen werden und beispielsweise von CDN jsDelivr. Findet ein Angreifer nun einen Weg, eine XSS-Attacke auszuführen, bei der er versucht, von einer anderen Domain als von den erlaubten Domains Schadcode einzuschleusen, blockiert CSP das Skript von der nicht freigegebenen Domain.

Dazu ein einfaches Beispiel: Wenn wir im <head> in Listing 2 gleich nach dem meta-Tag für die Seitencodierung die Zeile <meta http-equiv=”Content-Security-Policy” content=”default-src ‘self'” /> einfügen und die Datei ausführen, bekommen wir eine Ausgabe wie in Abbildung 2.

Abb. 2: Blocked Content

Der Teil content=”default-src ‘self'” sorgt dafür, dass sämtliche Inhalte, die nicht von der eigenen Domäne stammen, über CSP im Browser blockiert werden. Wollen wir zusätzlich für JavaScript das jsDelivr CDN zulassen, müssen wir den meta-Tag wie in folgt formulieren:

<meta http-equiv="Content-Security-Policy"
  content="default-src 'self';
  script-src 'nonce-rAnd0m' https://cdn.jsdelivr.net; />

Auf der offiziellen Webseite für Content Security Policy [4] finden Sie eine vollständige Auflistung sämtlicher Attribute und eine umfangreiche Liste an Beispielen. So bewirkt der Ausdruck nonce-rAnd0m dass Inline-Skripte geladen werden dürfen. Dazu muss das zugehörige JavaScript mit dem Attribut [script nonce=”rAnd0m”/] gekennzeichnet werden. Damit unser Beispiel aus Listing 2 vollständig funktioniert, benötigen wir für den CSP-meta-Tag folgenden Eintrag (Listing 3).

<meta http-equiv="Content-Security-Policy"
  content="default-src 'self';
  script-src 'nonce-rAnd0m' https://cdn.jsdelivr.net;
  img-src https://images.unsplash.com;
  style-src 'self' https://cdn.jsdelivr.net;
  frame-src https://www.youtube.com;
  font-src https://fonts.googleapis.com;" />

Für Java-Webapplikationen können die Headerinformationen zu den Beschränkungen der CSP über den response hinzugefügt werden. Die Zeile response.addHeader (“Content-Security-Policy”, “default-src ‘self'”); bewirkt wie zu Beginn, dass sämtliche Inhalte, die nicht von der eigene Domäne stammen, blockiert werden.

MEHR PERFORMANCE GEFÄLLIG?

Performance & Security-Track entdecken

 

Zentrale Regelung über Servlet-Filter

Nun ist das applikationsweite manuelle Hinzufügen von CSP-Regeln für jede eigene Seite recht mühselig und zudem noch sehr fehleranfällig. Eine zentrale Lösung für den Einsatz von Java-Servern ist die Verwendung von Servlet-Filtern (Listing 4).

public class CSPFilter implements Filter {

  public static final String POLICY = "default-src 'self'";

  @Override
  public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
  throws IOException, ServletException {
    if (response instanceof HttpServletResponse) {
      ((HttpServletResponse)response).setHeader("Content-Security-Policy", CSPFilter.POLICY);
    }
  }

  @Override
  public void init(FilterConfig filterConfig) throws ServletException { }

  @Override
  public void destroy() { }
}

Um den Filter dann zum Beispiel im Apache Tomcat zu aktivieren, wird noch ein kleiner Eintrag in der web.xml benötigt (Listing 5).

<filter>
  <filter-name>CSPFilter</filter-name>
  <filter-class>com.content-security-policy.filters.CSPFilter</filter-class>
</filter>
<filter-mapping>
  <filter-name>CSPFilter</filter-name>
  <url-pattern>/*</url-pattern>
</filter-mapping>

Sicher könnte man für sämtliche Applikationen in einem vorgeschalteten Proxyserver wie dem Apache-2-HTTP-Server oder NGNIX alle CSP-Regeln zentral verwalten. Davon ist allerdings aus zwei Gründen abzuraten: Zum einen verlagert sich so die Verantwortlichkeit von der Entwicklung hin zum Betrieb, was zu erhöhtem Kommunikationsaufwand und einem Flaschenhals in der Entwicklung führt. Zum anderen werden die so entstehenden Regeln sehr komplex und entsprechend schwieriger zu lesen beziehungsweise zu warten. Der hier vorgeschlagene Servlet-Filter ist daher eine gute Option, um die Sicherheitsregeln applikationsweit zentral für exakt die entwickelte Anwendung zu verwalten. Zudem passt das auch hervorragend in ein agiles DevOps-Konzept, denn die notwendige Konfiguration ist Bestandteil des Source Codes und über die Versionsverwaltung unter Konfigurationsmanagement zugänglich.

 

Fazit

Wie ich in diesem Artikel zeigen konnte, ist das Thema Web-Application-Security nicht immer gleich ein Fall für Spezialisten. Persönlich empfinde ich es als eine große Errungenschaft der letzten Jahre, wie das Härten von Webanwendungen kontinuierlich einfacher wird. CSP halte ich auf diesem Weg für einen Schritt in die richtige Richtung. Natürlich gilt es weiterhin, stets ein Auge auf aktuelle Entwicklungen zu haben, denn die bösen Buben und Mädels ruhen sich nicht auf ihren Lorbeeren aus und lassen sich ständig neue Gemeinheiten einfallen. Der gerade populär gewordene breite Einsatz von künstlichen neuronalen Netzen wie ChatGPT lässt im Moment nur grob erahnen, was uns künftig noch an Cyberangriffen aus den Tiefen des weltweiten Netzes erwarten wird.


Links & Literatur

[1] https://owasp.org/www-community/attacks/xss/

[2] https://www.w3.org/TR/CSP/

[3] https://de.wikipedia.org/wiki/Content_Delivery_Network

[4] https://content-security-policy.com

The post Sichere Web-Apps für alle appeared first on JAX.

]]>
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.

]]>
Alles im Blick https://jax.de/blog/alles-im-blick/ Fri, 11 Mar 2022 13:59:28 +0000 https://jax.de/?p=85794 Wie funktionieren APM Agents in der Java Virtual Machine im Detail? Was ist bei der Instrumentierung zu beachten und welche Besonderheiten von APM-Agenten muss man berücksichtigen? Und warum werden oftmals nur die bekanntesten Java-Frameworks unterstützt? Diesen Fragen gehen wir in diesem Beitrag auf den Grund.

The post Alles im Blick appeared first on JAX.

]]>
APM steht für Application Performance Management und erlaubt es als Teil der Observability, die eigene Anwendung genauer zu durchleuchten. Wie lange dauern bestimmte SQL-Abfragen? Welche Microservices oder Datenbanken werden innerhalb eines HTTP Requests abgefragt? Welcher Teil eines HTTP Requests ist der eigentliche Flaschenhals und weist die längste Antwortzeit auf? Um diese Fragen zu beantworten, reicht es nicht, Logs oder Metriken zu betrachten, sondern man muss sich die Laufzeit einzelner Methoden oder Aufrufe innerhalb der eigenen Anwendung ansehen. Hier kommt APM ins Spiel.

APM als Säule der Observability

APM gehört zum Tracing, das neben Logs und Metriken als eine der drei Säulen der Observability gilt. Dabei geht es allerdings nicht um die Daten selbst oder die Art der Datengenerierung, sondern allein um die Möglichkeit, aus vielen Signalen (Logs, Metriken, Traces, Monitoring, Health Checks) diejenigen herauszufiltern, die auf eine mögliche Einschränkung eines eigenen oder fremden Service wie Antwortzeit oder Verfügbarkeit hindeuten. Einzelne Bereiche der Observability sollten nicht isoliert betrachtet und idealerweise auch nicht mit unterschiedlichen Tools bearbeitet werden, damit man nicht nachts um drei Uhr mit mehreren Browsertabs und manueller Korrelation bei einem Ausfall eingreifen muss. Eine Kerneigenschaft des APM ist die Darstellung der Laufzeit von Komponenten der eigenen Anwendung. Die zwei wichtigsten Begriffe sind hier Transaction und Span. Eine Transaction ist eine systemübergreifende Zusammenfassung einzelner Spans, welche die Laufzeit konkreter Methoden oder Aktionen innerhalb eines Systems zusammenfassen. Eine Transaction kann sich über mehrere Systeme ziehen und beginnt bei einer Webanwendung im besten Fall im Browser des Users (Abb. 1).

Abb. 1: Transaction über mehrere Systemgrenzen (farbig gekennzeichnet)

Instrumentierung innerhalb der JVM

Bei APM-Agenten liegt der Fall etwas anders als bei Logging und Metriken, da sie in die Anwendung hineinschauen müssen und diese unter Umständen auch verändern. Bugs in diesen Agenten sind gefährlich und können sich auf die Anwendung auswirken – unabhängig von der Programmiersprache. Java hat im Gegensatz zu vielen anderen Sprachen eine standardisierte Schnittstelle zur Instrumentierung. Der durch Kompilierung erstellte Bytecode kann verändert werden und die Anwendung kann mit diesen Änderungen weiterlaufen. Um diese Veränderung so einfach wie möglich zu implementieren, gibt es Bibliotheken wie ASM oder Byte Buddy, mit denen Methodenaufrufe abgefangen werden können, um beispielsweise die Laufzeit zu messen. Wenn ich als Entwickler eines Agenten also alle Aufrufe des in der JVM eingebauten HTTP-Clients abfange und die Laufzeit sowie den Endpunkt als Teil eines Spans logge, kann ich danach einfach im APM UI sehen, wie viel Zeit diese Anfrage benötigt und ob lokales Request Caching beim Einhalten möglicher SLAs hilft. Durch Aktivieren des Agenten darf kein oder nur geringer Einfluss auf die Geschwindigkeit der Anwendung genommen werden (Overhead). Das Gleiche gilt für die Garbage Collection. Beides lässt sich nicht völlig verhindern, jedoch stark reduzieren.

Stay tuned

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

 

Der Zeitpunkt der Instrumentierung ist unterschiedlich. Die bekannteste und beliebteste Variante ist das Setzen des Agents als Parameter beim Starten der JVM:

java -javaagent:/path/to/apm-agent.jar -jar my-application.jar

Dieser Aufruf führt die Instrumentierung aus, bevor der eigentliche Code geladen wird. Alternativ kann die Instrumentierung auch bei einer bereits laufenden JVM stattfinden.

Doch was genau bedeutet Instrumentierung eigentlich? Die Laufzeit von Methoden kann nur gemessen werden, wenn diese Methoden abgefangen werden und Code des Agenten um den eigentlichen Code herum ausgeführt wird – zum Beispiel das Anlegen eines Spans oder einer Transaction, wenn ein HTTP Request abgeschickt wird. Dieser Ansatz ist aus der aspektorientierten Programmierung bekannt. Eine Voraussetzung muss gegeben sein: Der jeweilige APM-Agent muss die Methode inklusive Signatur kennen, die instrumentiert wird. Für Java Servlets ist zum Beispiel die Methode service(HttpServletRequest req, HttpServletResponse resp) im Interface HttpServlet die wichtigste Methode, um jeden HTTP-Aufruf zu überwachen, unabhängig vom Pfad oder der HTTP-Methode. Das bedeutet gleichzeitig, dass bei einer Änderung der Methodendeklaration der Agent ebenfalls angepasst werden muss. Und hier liegt eines der größten Probleme mit dieser Form der Überwachung: Es muss sichergestellt sein, dass sowohl möglichst viele Frameworks (Spring Boot, JAX-RS, Grails, WildFly, Jetty etc.) und deren Methoden instrumentiert werden, als auch ständig überprüft wird, ob die Instrumentierung bei einem neuen Release noch funktioniert, besonders bei Major Releases. Das ist eine der großen Maintenance-Aufgaben bei der Entwicklung von APM-Agenten. Während es einige Standards im Java-Bereich gibt, die einfach zu instrumentieren sind, wie Servlets, JDBC oder JAX-RS, gibt es ebenfalls eine Menge Frameworks, die keinen solchen Standards folgen, wie beispielsweise Netty. Das heißt auch, dass jeder Agent, dessen Instrumentierung eines Frameworks nicht funktioniert hat, weil eventuell Methoden aus einer anderen Major-Version mit anderer Signatur existieren, sicherstellen muss, dass dies kein Problem darstellt.

Bisher haben wir nur APM Agents im Kontext von Java Agents erwähnt; es gibt aber noch weitere Anwendungsfälle. Ganz aktuell hat das AWS-Corretto-Team um Volker Simonis einen Agent zum Patchen der Log4Shell-Sicherheitslücke entwickelt [1]. Der Agent verhindert das Sicherheitsproblem der Remote Code Execution bei einer bereits laufenden JVM. Ein anderer Use Case sind Agents, die Sicherheitsfeatures wie das automatische Setzen von HTTP Headern oder WAF-Funktionalität bereitstellen.

 

Agents und ihre Features

Der bekannteste Standard in der Observability-Welt ist OpenTelemetry [2]. Die Idee von OpenTelemetry ist, den einen, herstellerneutralen, universellen Standard zur Verfügung zu stellen, den alle verwenden, unabhängig von der Programmiersprache. Mit dem OpenTelemetry Agent gibt es einen JVM Agent, den viele Observability-Anbieter als eigene Distribution veröffentlichen, wie zum Beispiel Lightstep oder Honeycomb. Dieser kommt bereits mit einigen Instrumentierungen für bekannte Frameworks [3].

Eine weitere Standardisierung in Zeiten von Microservices und der Möglichkeit, einen Request vom Laden der Webseite im Browser bis zur SQL-Query zu identifizieren, ist das Distributed Tracing. Um Distributed Tracing zwischen verschiedenen Programmiersprachen und Umgebungen zu implementieren, existiert die OpenTracing-Spezifikation und -Implementierung. Viele APM Agents folgen dieser Spezifikation, um Kompatibilität sicherzustellen, unter anderem Lightstep, Instana, Elastic APM, Apache Skywalking und Datadog.

Vor OpenTelemetry gab es einige wenige Agents, die aus JVM-Sicht eigentlich gar keine waren, weil sie keinerlei Instrumentierung vorgenommen, sondern lediglich Interfaces in bestimmten Frameworks implementiert haben, um Monitoringdaten auszulesen. Ich gehe davon aus, dass es über kurz oder lang nur noch Agents geben wird, die auf dem OpenTelemetry Agent basieren, und dass die Alleinstellungsmerkmale nicht im Sammeln der Daten, sondern ausschließlich in der Auswertung liegen.

Elastic APM Agent

Wenn es einen OpenTelemetry Agent gibt, wieso gibt es dann zum Beispiel auch einen Elastic-APM-spezifischen Agent? Zum einen gibt es eben doch mehr Features als einen universellen Standard. Beispiel: das Erfassen interner JVM-Metriken (Speicherverbrauch, Garbage-Collection-Statistiken) oder auch das Auffinden langsam ausführender Methoden ohne das Wissen um konkrete Methoden oder Code mit Hilfe des async-profilers [4] – eine Technologie, die bei Datadog, Elastic APM oder Pyroscope [5] verwendet wird. Des Weiteren existieren eine Menge Agenten bereits länger als die OpenTelemetry-Implementierung und bringen mehr Unterstützung für bestimmte Frameworks mit, die erst in den OpenTelemetry-Agenten portiert werden müssen.

Der Elastic Agent bietet zudem ein weiteres sehr interessantes Feature, und zwar das programmatische Konfigurieren des Agent anstatt der Verwendung des JVM-Agent-Mechanismus als Parameter beim Starten der JVM. Das bedeutet, man bindet den Agent als Dependency in den Code ein, und versucht die folgende Zeile Code beim Start der Anwendung so früh wie möglich auszuführen:

ElasticApmAttacher.attach();

Jetzt geschieht prinzipiell dasselbe wie bei der Agent-spezifischen Konfiguration: Der Agent attacht sich selbst an den laufenden Code. Diese Art der Einrichtung hat einen großen Vorteil: Die Dependency ist bereits Teil des Deployments und muss nicht als Teil des Build-Prozesses oder der Container-Image-Erstellung heruntergeladen werden. Gleichzeitig ist der Entwickler für das fortlaufende Aktualisieren verantwortlich.

Bisher ungeklärt ist die Frage, was ein APM Agent mit den erhobenen Daten eigentlich machen soll. Im Fall von Elastic APM werden diese an einen APM-Server geschickt, der sie wiederum im nächsten Schritt in einem Elasticsearch Cluster speichert. Der APM-Server kommuniziert nicht nur mit den anderen Elastic APM Agents (Node, Ruby, PHP, Go, iOS, .NET, Python), sondern kann auch Daten puffern und als Middleware für Source Mapping bei JavaScript-Anwendungen agieren.

Wie bereits erwähnt, liegt der Mehrwert weniger im Sammeln als im Auswerten von Daten. Im Fall von Elastic APM ist das unter anderem die Integration mit Machine Learning, genauer der Time Series Anomaly Detection, um automatisiert Laufzeiten von Transaktionen zu erkennen, die im Vergleich zu vorher gemessenen Ergebnissen überdurchschnittlich lange brauchen, dem automatischen Annotieren von Deployments im APM UI oder auch der Korrelation von plötzlich auftretenden Transaktionslatenzen und Fehlerraten in allen von der Anwendung generierten Logs. So wird sichergestellt, dass die Grenzen zwischen den anfangs erwähnten Observability-Säulen nicht existieren.

Programmatische Spans und Transactions

Nicht jeder Entwickler möchte ein eigenes APM-Agent-Plug-in schreiben, damit die eigene Java-Anwendung Spans und Transactions innerhalb der eigenen Geschäftslogik verwendet. Ein alle 30 Sekunden laufender Job im Hintergrund sollte als eigene Transaktion und jede der darin aufgerufenen Methoden als eigener Span konfiguriert werden. Hier gibt es zwei Möglichkeiten der Konfiguration. Entweder werden die Methodennamen über die Agentenkonfiguration angegeben oder man wählt die programmatische Möglichkeit. Ein Beispiel innerhalb von Spring Boot zeigt Listing 1.

@Component
public class MyTask {
 
  @CaptureTransaction
  @Scheduled(fixedDelay = 30000)
  public void check() {
    runFirst();
    runSecond();
  }
 
  @CaptureSpan
  public void runFirst() {
  }
 
  @CaptureSpan
  public void runSecond() {
  }
}

Die Annotation @CaptureTransaction legt eine neue Transaktion an und innerhalb dieser Transaktion werden die beiden Spans für die Methoden via @CaptureSpan angelegt. Sowohl Transaktion als auch Spans können mit einem eigenen Namen konfiguriert werden, der im UI einfacher identifiziert werden kann. Unter [6] gibt es ein GitHub Repository, das sowohl die Instrumentierung des Java-HTTP-Clients zeigt als auch das Verwenden von programmatischen Transaktionen und Spans im eigenen Java-Code.

 

Elastic APM Log Correlation

Wie erwähnt, ist es sinnvoll, Logs, Metriken und Traces miteinander zu verbinden. Wie aber kann eine bestimmte Logzeile mit einer bestimmten Transaktion verbunden werden? In Elastic APM heißt dieses Feature Log Correlation. Der erste (optionale) Schritt ist, Logdateien ins JSON-Format zu überführen. Das macht es wesentlich einfacher, weitere Felder zu den Logdaten hinzuzufügen. Eben diese Felder werden für die Korrelation benötigt. Wenn man im Agent die Option log_ecs_reformatting verwendet, werden im sogenannten MDC der jeweiligen Logger-Implementierung die Felder transaction.id, trace.id und error.id hinzugefügt, nach denen dann sowohl in Transactions und Spans als auch in einzelnen Lognachrichten gesucht werden kann. So können Lognachrichten unterschiedlichster Services miteinander korreliert und durchsucht werden; Logmeldungen eines Service sind einer konkreten eingehenden HTTP-Anfrage zuzuordnen.

Automatische Instrumentierung mit K8s

Will man Container mit Java-Anwendungen unter Kubernetes instrumentieren, kann man jederzeit die verwendeten Images/Pod-Konfigurationen anpassen und innerhalb dieser den Agent konfigurieren sowie APM-Konfigurationen einstellen, zum Beispiel APM-Endpunkte, API-Token (zum Beispiel via HashiCorp Vault [7]) oder die erwähnte Log Correlation. Es gibt eine weitere Möglichkeit, und zwar die Verwendung eines Init-Containers, der vor den eigentlichen Anwendungscontainern in einem Pod ausgeführt wird [8]. Dieser Container konfiguriert Umgebungsvariablen, die dann beim Starten des regulären Containers ausgelesen werden und somit zusätzlich den passenden JVM-Agenten starten [9]. Dieser Ansatz kann sinnvoll sein, wenn man keine Kontrolle über die erstellten Container hat oder sicherstellen möchte, dass ein Agent in einer bestimmten Version für alle Java-Anwendungen läuft.

MEHR PERFORMANCE GEFÄLLIG?

Performance & Security-Track entdecken

 

Distributed Tracing mit RUM

In Zeiten von Microservices und APIs ist es in vielen Systemarchitekturen wahrscheinlich, dass ein einzelner Aufruf eines Anwenders sich zu mehreren Aufrufen in der internen Architektur multiplext und mehrere Services abgefragt werden. Hier ist es besonders wichtig, verfolgen zu können, wie ein initialer Request durch die unterschiedlichen Services weitergereicht und verändert wird. Eine Transaktion kann mehrere Spans haben, die in unterschiedlichen Systemen auftreten, unter Umständen auch gleichzeitig. Hier kommt Distributed Tracing mit Hilfe von Trace IDs ins Spiel, die durch alle Requests hindurch – zum Beispiel mit Hilfe von HTTP-Headern – an die jeweiligen Spans angehängt werden und somit durch den Lebenszyklus des initialen Request rückverfolgbar sind. Ein weiterer Vorteil von Distributed Tracing ist die Möglichkeit, aus diesen Daten eine Service Map zu erstellen, da man weiß, welche Services miteinander kommunizieren (Abb. 2).

Abb. 2: Service Map, um Kommunikationsflüsse einzelner Komponenten zu visualisieren

Bei der Entwicklung von Webanwendungen ist es ebenfalls nicht ausreichend, erst an den eigenen Systemgrenzen mit dem Anlegen von Transactions und Spans zu beginnen, da man sonst keinen Überblick über die komplette Performance der eigenen Anwendung hat. Wie lange dauert das Aufbauen der Verbindung im Browser zum Webserver? Ist die Latenz hier eventuell so hoch, dass es irrelevant ist, 50 ms bei einer komplexen SQL-Query zu sparen? Um dieses Problem anzugehen, gibt es das Real User Monitoring, kurz RUM. Zum einen können Transaktionen an der richtigen Stelle begonnen werden, zum anderen werden auch Browserereignisse geloggt, um festzustellen, wie lange das initiale Rendern der Seite braucht, sodass der Anwender mit der Anwendung interagieren kann (Abb. 3).

Abb. 3: RUM-Dashboard mit Ladezeiten und Browserstatistiken

APM in der Zukunft

Das Bedürfnis, für Anwendungen eine Art Röntgengerät zu bekommen, wird in Zukunft noch zunehmen – vielleicht werden sich die Methoden etwas ändern. Zeit für einen kleinen Ausblick. In den vergangenen Jahren ist eine neue Art von Agents auf den Markt gekommen, die eine neue, sprachunabhängige Technologie verwenden: eBPF. Mit Hilfe von eBPF kann man Programme im Kernelspace laufen lassen, ohne den Kernel zu verändern oder ein Linux-Kernel-Modul laden zu müssen. Alle eBPF-Programme laufen innerhalb einer Sandbox, sodass das Betriebssystem Stabilität und Geschwindigkeit garantiert. Da eBPF Syscalls überwachen kann, ist es ein idealer Einstiegspunkt für jegliche Observability-Software. Der weitaus wichtigere Teil ist allerdings die Möglichkeit, diese Syscalls auf Methodenaufrufe in die jeweilige Programmiersprache des überwachten Programms zu übersetzen. eBPF-basierte Profiler haben generell einen geringen Overhead, da sie sehr tief im System verankert sind. Des Weiteren müssen keine Deployments angepasst werden, da diese Profiler auch innerhalb eines Kubernetes-Clusters für alle Pods konfiguriert werden können. Beispiele für diese Art von Profiler sind prodfiler [10] von Elastic, Pixie [11], Parca [12] oder Cilium Hubble [13].

Ein weiteres neues Themenfeld ist das Überwachen auf Serverless-Plattformen. Hier braucht man etwas andere Lösungen, da nicht garantiert ist, dass nach dem Verarbeiten einer Anfrage noch Rechenkapazität zur Verfügung gestellt wird. Methoden wie etwa Spans und Traces als Batch zu sammeln und periodisch an den APM-Server zu verschicken, funktionieren hier also nicht. Für AWS Lambda steht mit opentelemetry-lambda [14] ein eigenes GitHub-Projekt zur Verfügung. Die grundlegende Idee ist ein sogenannter Lambda-Layer, der diese Observability-Aufgaben übernimmt. Wenn man also in diese Art von Plattformen eintaucht, sollte man sicherstellen, dass die eigene Observability-Plattform diese Technologien unterstützt.

Ein weiterer wichtiger Baustein abseits vom Sammeln und Auswerten der Livedaten ist der Trend zu Shift Left – nicht nur in der Security. Hier bietet JfrUnit [15] von Gunnar Morning einen interessanten Ansatz aus dem Umfeld des Java Flight Recorders. Als Teil des Unit Testings werden JFR Events herangezogen, um bestimmte Constraints wie Garbage Collection, erhöhte Memory Allocation oder I/O bereits in Tests festzustellen und vor dem eigentlichen Deployment zu korrigieren.

Stay tuned

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

 

Schlusswort

Wie überall, so ist auch in der Welt der JVM Agents für APM nicht alles rosig. Einige Agents unterstützen zum Beispiel nur die bekanntesten Web-Frameworks wie Spring oder Spring Boot bzw. auch innerhalb eines Frameworks nur synchrones Request Processing. Es gilt daher, anfangs in Ruhe mögliche Agents zu testen. Fast alle Agents sind Open Source, sodass man im Fall der Fälle auch ein eigenes Plug-in schreiben kann. Je nach Sicherheitseinstellungen der Plattform, auf der Services betrieben werden, ist es eventuell nicht erlaubt, einen Agent programmatisch an den Java-Prozess anzuhängen – zum Beispiel ist mir das bei der Digital-Ocean-Apps-Plattform, einem PaaS, nicht gelungen. Der -javaagent-Parameter innerhalb des Docker Image hat hingegen einwandfrei funktioniert.

Ein weiteres Thema, das man in den aktuellen Java-Trends wahrscheinlich schon entdeckt hat, ist GraalVM. Falls mit Hilfe der GraalVM die Anwendungen in native Binaries umgewandelt werden, existiert der Mechanismus zum Anhängen von Java Agents nicht. Das heißt nicht, dass keinerlei Instrumentierung möglich ist. Die programmatische Erstellung von Spans und Traces könnte allerdings bei einigen APM-Lösungen noch funktionieren, die nicht auf reines Bytecode Enhancement setzen. Da viele bekanntere Frameworks wie Spring und Quarkus inzwischen native Extensions und Module haben, um möglichst einfache Binaries zu erstellen, erwarte ich in den nächsten Monaten, dass auch die APM-Plattformen nachziehen werden. Quarkus hat bereits Support für OpenTracing und DataDog im native Mode.

Um es noch einmal abschließend zu wiederholen: Observability ersetzt kein Monitoring und APM ersetzt kein effizientes Entwickeln performanter Software. Viele Probleme können durch Testing, Reviews oder Pair Programming sehr viel früher im Lebenszyklus der Software gefunden werden und sind dann weitaus ökonomischer zu fixen. Wesentlich komplizierter ist das bereits bei Distributed Tracing und dessen Intersystemkommunikation, um mögliche Bottlenecks vor dem Produktionsbetrieb zu identifizieren. Nichtsdestoweniger ist ein so tiefer Einblick in die selbstgeschriebene Software, wie APM ihn bietet, von Vorteil und sollte auch genutzt werden, wenn der zusätzliche Aufwand der initialen Einrichtung einmal erledigt ist.

 

Links & Literatur

[1] https://github.com/corretto/hotpatch-for-apache-log4j2

[2] https://opentelemetry.io

[3] https://github.com/open-telemetry/opentelemetry-java-instrumentation/blob/main/docs/supported-libraries.md#libraries–frameworks

[4] https://github.com/jvm-profiling-tools/async-profiler

[5] https://pyroscope.io

[6] https://github.com/spinscale/observability-java-samples

[7] https://www.vaultproject.io/

[8] https://kubernetes.io/docs/concepts/workloads/pods/init-containers/

[9] https://www.elastic.co/blog/using-elastic-apm-java-agent-on-kubernetes-k8s

[10] https://prodfiler.com

[11] https://px.dev

[12] https://www.parca.dev

[13] https://github.com/cilium/hubble

[14] https://github.com/open-telemetry/opentelemetry-lambda

[15] https://github.com/moditect/jfrunit

The post Alles im Blick appeared first on JAX.

]]>
IT-Forensik: “Software kann Tatort oder Tatmittel sein” https://jax.de/blog/it-forensik-software-kann-tatort-oder-tatmittel-sein/ Tue, 01 Oct 2019 11:54:26 +0000 https://jax.de/?p=72915 Das Ziel, möglichst sichere IT-Systeme zu bauen, ist Entwicklern nicht fremd – zumindest theoretisch. Was aber, wenn das Kind schon in den Brunnen gefallen ist und die Software bereits angegriffen wurde? Dann helfen IT-Forensiker wie Martin Wundram, der uns im W-JAX-Interview eine kleine Einführung gibt und erklärt, warum IT-Forensik auch Entwickler und Software-Architekten angeht.

The post IT-Forensik: “Software kann Tatort oder Tatmittel sein” appeared first on JAX.

]]>
IT-Forensik – was ist das eigentlich?

Redaktion: Hallo Martin. Du sprichst auf der W-JAX über IT-Forensik. Dabei geht es ja explizit nicht um das Bauen möglichst sicherer IT-Systeme. Worum geht es dann?

In jedem Fall suchen wir nach digitalen Spuren.

Martin Wundram: IT-Systeme und deren Programme werden leider immer wieder etwa bei Industriespionage angegriffen, oder einfach missbräuchlich verwendet, z.B. um illegitim an geheime Informationen eines Unternehmens zu gelangen. Die betroffenen Unternehmen, IT-Experten, externe Beteiligte brauchen dann Antworten auf die typischen W-Fragen: Wer hat wann was gemacht? Wie wurden welche Informationen wohin übertragen?

Software kann dabei Tatort oder Tatmittel sein. In jedem Fall suchen wir nach digitalen Spuren. Mein Talk legt dabei die Perspektive auf Entwickler und Architekten, und was man tun kann, um Software für solche Fälle möglichst gut vorzubereiten, damit IT-Forensiker später einmal möglichst zuverlässig wertvolle Spuren finden, verstehen und auswerten können. Und es geht auch darum, zu überlegen, wie Entwickler selbst Ziel von Angriffen werden könnten.

 
Redaktion: Warum betrifft IT-Forensik auch Entwickler und Architekten?

Martin Wundram: IT-Forensik betrifft insofern auch Entwickler und Architekten, dass sie sich und ihre Umgebungen zum einen selbst schützen und zum anderen bei eventuellen Angriffen auf ihre eigene IT wirksam reagieren können müssen. Und wenn bereits in Design und Entwicklung einer Software wichtige Aspekte von IT-Forensik, z.B. aussagekräftige Protokollierung/Logging, berücksichtigt werden, dann hilft das später “im Einsatz” oft sehr.

 

Recht auf Privatsphäre

Redaktion: Wie gehen IT-Forensiker typischerweise vor? Welche Methoden & Tools kommen da zum Einsatz?

Martin Wundram:  Maximal verkürzt dargestellt: sichern, analysieren, präsentieren. Wenn möglich, erstellen IT-Forensiker beweiskräftige, sogenannte forensische Kopien von Datenträgern oder Datenbeständen. Ein wesentlicher Unterschied liegt darin, ob “Tot-Forensik” zum Einsatz kommt, es handelt sich dabei z.B. um ausgeschaltete Datenträger, oder “Live-Forensik”, also die unmittelbare Arbeit am und mit einem laufenden System.

Als Tools kommen sowohl kommerzielle als auch Open-Source-Software zum Einsatz. Interessant ist, dass nicht immer Spezial-Software verwendet wird, sondern auch “Alltags-Software”, etwa normale System-Programme bei einer Live-Analyse eines laufenden Systems.

 
Redaktion: Du erwähnst auch das Spannungsfeld zwischen erwünschter Aufklärung und „Ausschnüffeln“. Wie sind diesbzgl. deine Erfahrungen? Weshalb kann es da zu Spannungen kommen?

Es gilt, eine Balance aus Art und Menge der Datenspeicherung und des Zugriffs im “Bedarfsfall” zu finden.

Martin Wundram: Jeder Mensch hat grundsätzlich das Recht auf Privatheit und Intimsphäre. “Big Brother is watching you” ist keine angenehme Vorstellung. Spätestens, wenn es um die PIN zur Kreditkarte oder das geheime Rezept eines Getränkeherstellers geht, wird das sehr deutlich. Was aber, wenn wir Opfer einer Straftat werden, wenn jemand unser E-Mail-Postfach oder gleich den ganzen PC “hackt”? Dann wünschen wir uns doch auch eine erfolgreiche Aufklärung und nicht bloß die Erkenntnis, dass irgendeine IP-Adresse der Ursprung des Angriffs ist. Hier eine Balance aus Art und Menge der Datenspeicherung und des Zugriffs im “Bedarfsfall” zu finden, ist das herausfordernde Spannungsfeld. Welche Daten soll ein IT-System speichern? Wie lang? Wo? Mit welchen Zugriffsmöglichkeiten?
 
Redaktion: Und wie kann man diese Spannungen entschärfen?

Martin Wundram: Ein Weg sind offener Umgang mit dem Thema, Aufklärung der Betroffenen und Implementierung einfacher Auswahlmöglichkeiten für Art und Umfang von gewünschter Protokollierung. Natürlich lässt sich das Logging von Serversoftware seit jeher meist umfangreich einstellen. Aber aus Sicht der Anwender z.B. von Cloud-Anwendungen schaut es dann vielleicht mau aus. Bildhaft gesprochen: Es ist nicht leicht, das Pendel in der Mitte zu halten, und manchmal schwingt es irgendwer oder irgendwas ganz schön hin und her…
 
Redaktion: Was ist die Kernbotschaft deiner Session, die jeder Teilnehmer mit nach Hause nehmen sollte?

Martin Wundram: Weder Angst noch Kopf-in-den-Sand sind geeignet. Natürlich gibt es wichtigere Themen als IT-Forensik, aber wenn es dann doch einmal zu einem Vorfall kommt, kann verlässlich und nachvollziehbar arbeitende, gründlich entwickelte Software mit einem geeigneten Maß an Logging vielleicht überhaupt erst die Aufklärung ermöglichen oder zumindest ausreichend effizient machen. Daher ist die Kernbotschaft meiner Session: Vorbereitung ist die beste Medizin – oft hilft schon etwas Aufwand im Vorfeld mit großem Effekt in der Zukunft. Und schützt eure eigenen Systeme!
 
Redaktion: Vielen Dank für dieses Interview!

 

Quarkus-Spickzettel


Quarkus – das Supersonic Subatomic Java Framework. Wollen Sie zeitgemäße Anwendungen mit Quarkus entwickeln? In unserem brandaktuellen Quarkus-Spickzettel finden Sie alles, was Sie zum Loslegen brauchen.

 

Jetzt herunterladen!

The post IT-Forensik: “Software kann Tatort oder Tatmittel sein” appeared first on JAX.

]]>