~/blog/2013/07/nach-prism-verschluesselung-in-eig.md

Nach PRISM: Verschlüsselung in eigenen Anwendungen

5 min
Titelbild: Nach PRISM: Verschlüsselung in eigenen Anwendungen

Keine Zeit? Hier die Kurzversion in 30 Sekunden.

  • Seit Anfang Juni die PRISM-Enthüllungen: Als Entwickler kann ich keine Politik machen, aber meine eigenen Anwendungen sauber absichern.
  • TLS auch intern zwischen Diensten, Zertifikatsprüfung nie abschalten, Cipher-Suites mit Forward Secrecy (ECDHE/DHE) bevorzugen. Java 7 kann ECDHE, Java 6 kaum.
  • Daten at rest mit AES im GCM-Modus (oder CBC plus HMAC), niemals ECB. Passwörter nur gehasht, am besten mit bcrypt.
  • Das eigentliche Problem ist die Schlüsselverwaltung, nicht der Algorithmus.
  • Verschlüsselung ist kein Allheilmittel: Metadaten bleiben sichtbar.

Seit Anfang Juni die Meldungen über PRISM und die Überwachung der Internetkommunikation durch Geheimdienste. Über die politische Seite sollen andere schreiben, dazu fehlt mir die Kompetenz. Was ich kann, ist die Frage stellen, die mich als Entwickler betrifft: Was tue ich in meinen eigenen Anwendungen, damit Daten nicht im Klartext durch Netze und über Festplatten wandern? Die Antwort ist unspektakulär. Meistens fehlt nicht das Wissen, sondern die Konsequenz.

TLS auch nach innen

Die Verbindung zwischen Browser und Server ist heute meist per HTTPS gesichert, zumindest dort, wo ein Login im Spiel ist. Weniger selbstverständlich ist es bei der Kommunikation zwischen Diensten. Der Webserver redet mit dem Anwendungsserver, der mit der Datenbank, dazu kommen Schnittstellen zu Partnern. Intern gilt oft die Annahme, das Netz sei vertrauenswürdig. Ich halte das für eine bequeme Annahme und nicht für eine Sicherheitsmaßnahme. Ein einziger kompromittierter Rechner im Netz genügt, und aus dem internen Netz wird ein offenes. Die meisten Datenbanken und Message-Broker können TLS, es ist nur selten eingeschaltet.

Der häufigste Fehler steckt aber im Client. Wenn ein Zertifikat nicht akzeptiert wird, ist die schnellste Lösung ein TrustManager, der alles durchwinkt, oder ein verify_peer auf false. Das Ergebnis ist eine Verschlüsselung, die jeder Angreifer in der Mitte problemlos aufbrechen kann, weil niemand mehr prüft, mit wem man eigentlich spricht. Dann lieber das selbst signierte Zertifikat in den Truststore aufnehmen. Das sind fünf Minuten Arbeit, und danach stimmt die Sicherheit wirklich.

Forward Secrecy

Ein Punkt, der erst jetzt breiter diskutiert wird: Forward Secrecy. Bei einem klassischen RSA-Schlüsselaustausch reicht der private Schlüssel des Servers, um aufgezeichneten Verkehr nachträglich zu entschlüsseln. Mit den Cipher-Suites auf Basis von DHE oder ECDHE wird für jede Sitzung ein flüchtiger Schlüssel ausgehandelt, und der Serverschlüssel allein genügt nicht mehr. Wer Mitschnitte auf Vorrat sammelt, hat dann trotz späterem Schlüsseldiebstahl nichts in der Hand.

Für Java gibt es dabei einen Haken. Java 7 beherrscht ECDHE in den Standard-Providern, Java 6 kaum. Wer auf Java 6 sitzt, bekommt Forward Secrecy praktisch nur über DHE, und das ist langsamer. Das ist ein weiterer Grund, die Migration auf Java 7 nicht weiter zu verschieben. Auf Serverseite (Apache, nginx) gilt: Suites mit ECDHE und DHE nach oben sortieren und die Serverreihenfolge verbindlich machen, damit der Client sie nicht wieder nach unten wählt. Den Effekt kann man mit den üblichen Online-Tests zur TLS-Konfiguration prüfen.

Daten at rest

Verschlüsselte Übertragung hilft wenig, wenn die Daten danach im Klartext in der Datenbank oder in Dateien liegen. Für vertrauliche Felder oder Dokumente nehme ich AES. Entscheidend ist der Betriebsmodus. ECB sollte man grundsätzlich vermeiden, weil gleiche Klartextblöcke zu gleichen Chiffreblöcken werden und Muster sichtbar bleiben. CBC ist in Ordnung, braucht aber zusätzlich einen HMAC, sonst lässt sich der Chiffretext unbemerkt verändern. Einfacher ist GCM, das Verschlüsselung und Integritätsprüfung in einem Schritt erledigt. Seit Java 7 geht das in der Standardbibliothek:

import java.security.SecureRandom;
import javax.crypto.Cipher;
import javax.crypto.SecretKey;
import javax.crypto.spec.GCMParameterSpec;

public static byte[] verschluesseln(SecretKey key, byte[] klartext) throws Exception {
    byte[] iv = new byte[12];
    new SecureRandom().nextBytes(iv);

    Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
    cipher.init(Cipher.ENCRYPT_MODE, key, new GCMParameterSpec(128, iv));
    byte[] chiffre = cipher.doFinal(klartext);

    byte[] ergebnis = new byte[iv.length + chiffre.length];
    System.arraycopy(iv, 0, ergebnis, 0, iv.length);
    System.arraycopy(chiffre, 0, ergebnis, iv.length, chiffre.length);
    return ergebnis;
}

Wichtig sind zwei Dinge. Der Initialisierungsvektor (IV) muss für jede Nachricht neu und zufällig sein, bei GCM ist die Wiederverwendung mit demselben Schlüssel fatal. Und er wird vor den Chiffretext gestellt, denn er ist nicht geheim, wird aber zum Entschlüsseln gebraucht. Beim Entschlüsseln liest man die ersten zwölf Bytes als IV und übergibt dieselbe GCMParameterSpec. Wurde etwas manipuliert, wirft doFinal eine Exception. Genau das will man.

Schlüssel und Passwörter

Der Code oben ist der leichte Teil. Schwierig ist die Frage, wo der Schlüssel liegt. Steht er im Quelltext oder in einer Konfigurationsdatei neben den verschlüsselten Daten, hat man eine Fassade gebaut. Der Schlüssel gehört getrennt aufbewahrt: in einem Keystore mit eigenem Passwort, auf einem anderen System oder in einem Hardware-Modul, wenn der Aufwand sich rechtfertigt. Dazu kommen Rotation und die Frage, was nach dem Verlust passiert. Ich habe den Eindruck, dass an dieser Stelle die meisten Konzepte enden, obwohl hier die Sicherheit entschieden wird.

Bei Passwörtern ist die Regel einfacher: nie verschlüsseln, nur hashen, und zwar mit einem langsamen, gesalzenen Verfahren. Warum bcrypt dafür besser geeignet ist als ein einfaches SHA, hatte ich letztes Jahr schon beschrieben. Daran hat sich nichts geändert, es ist nur dringender geworden.

Grenzen

Ich möchte nichts versprechen, was Verschlüsselung nicht leisten kann. Sie schützt Inhalte, nicht die Tatsache der Kommunikation. Wer mit wem, wann und wie oft spricht, bleibt sichtbar, und gerade diese Metadaten sind für eine Auswertung oft aufschlussreicher als der Inhalt. Dazu kommen Endpunkte: Ein kompromittierter Rechner liest den Klartext, bevor er verschlüsselt wird. Und eine Hintertür in einem Produkt, das man nicht prüfen kann, hilft auch die beste Kryptografie nicht.

Trotzdem: Jede saubere TLS-Konfiguration und jedes sorgfältig verschlüsselte Feld macht massenhaftes Mitlesen teurer. Das ist wenig, aber es ist das, was ich selbst beeinflussen kann.

Beitragsbild: eigene Collage mit Logos von Wikimedia Commons (OpenSSL-Logo: OpenSSL-Projekt, gemeinfrei); die Marken gehören ihren Inhabern.

$ whoami
Christian Ackermann

Softwareentwickler (C++, C, Java, C#) aus der Oberpfalz, bloggt seit 2001 über Linux, Technik und Kochen.