Aus Linux-Magazin 01/2012

Programmieren mit der Java-SE-Security-Bibliothek

© Pavel Ignatov, 23RF.com

Die Vorbeugung gegen Datendiebstahl und illegale Zugriffe spielt bei der Anwendungsentwicklung eine immer größere Rolle. Der Java-Programmierer findet in der Java-SE-Security-Bibliothek, was er braucht, um Daten zu ver- und entschlüsseln sowie Prüfsummen und Signaturen einzusetzen.

Sicherheitsaspekte werden bei der Produktion von Software immer wichtiger. Inzwischen stehen sie gleichberechtigt neben den funktionalen Anforderungen und bestimmen nicht selten die Software-Architektur. Sicherheitsfragen umfassen ein weites Feld, sei es der unberechtigte Zugriff Dritter auf Daten, unbemerkte Änderungen daran oder das Ausführen ungewollter Fremdprogramme.

Um dies zu verhindern, bietet Java in der Security-Bibliothek eine Reihe von Funktionalitäten an. Das reicht von der klassischen Verschlüsselung über digitale Signaturen bis hin zu eingeschränkten Rechten für Java-Programme selbst. Dies ist Teil der normalen Java-Distributionen (Open JDK und Oracle-Java) und in den Paketen und »javax.security« und »javax.crypto« zu finden.

Den Aufbau der Pakete gibt die Java Cryptography Architecture (JCA, [1]) vor. Den Kern bildet eine Handvoll Klassen für Sicherheitsaufgaben wie etwa Verschlüsselung oder Signatur. Die Klasseninstanzen erzeugt der Programmierer nicht wie üblich mit »new« , sondern er ruft sie bei einer Factory-Methode für den gewünschten Algorithmus ab. Dadurch ist pro Aufgabe unabhängig von der Anzahl verfügbarer Algorithmen nur noch eine Klasse erforderlich.

Krypto-Alternative

Diese Flexibilität reicht über die Algorithmen hinaus bis hin zur verwendeten Implementierung. So steht als Alternative zu Oracles Umsetzung unter anderem die umfangreichere und MIT-lizenzierte Ausgabe der Programmierergruppe Legion of the Bouncy Castle [2] zur Verfügung. Der Preis für diese Flexibilität liegt auf der Hand: Anfragen mit falschen Namen bestraft Java mit der Ausnahme »NoSuchAlgorithmException« . Abbildung 1 zeigt die in der JDK-Version 7 verfügbaren Algorithmen, das zugehörige Programm findet sich in den Beispiel-Quelltexten zu diesem Artikel [3].

Es liegt nahe, sensible Daten durch Verschlüsseln zu schützen. Symmetrische Verfahren wie der Advanced-Encryption-Algorithmus (AES) verwenden dabei zweimal den gleichen Schlüssel: Um die Originaldaten in eine unlesbare Form zu verwandeln und um sie wieder zu entschlüsseln (Abbildung 2).

Geheim!

Den typischen Aufbau für Ver- und Entschlüsselprogramme in Java zeigt Listing 1. Es führt die Operationen über ein »Cipher« -Objekt durch, das es für den gewünschten Algorithmus über die »getInstance()« -Methode erzeugt (Zeile 1). Das Beispiel nutzt AES mit einer Schlüssellänge von 128 Bit. Passend zu »Cipher« erzeugt der »Keygenerator« einen Schlüssel, entweder – wie im Listing – als zufälligen Einmalschlüssel oder basierend auf einem Passwort. Vor der Verwendung muss der Programmierer das Cipher-Objekt mit dem Schlüssel für die gewünschte Richtung initialisieren (Zeile 8).

Listing 1

Symmetrische Verschlüsselung mit AES

01 Cipher cipher = Cipher.getInstance("AES-128");
02
03 KeyGenerator kgen = KeyGenerator.getInstance("AES-128");
04 kgen.init(128);
05
06 SecretKey skey = kgen.generateKey();
07
08 cipher.init(Cipher.ENCRYPT_MODE, skey);
09
10 String klartext = "Die Botschaft";
11 System.out.println("Klartext " + klartext );
12
13 byte[] zuVerschluesseln =klartext.getBytes();
14 // Die Verschlüesselung
15 byte[] verschluesselt = cipher.doFinal( zuVerschluesseln );
16 // Kodieren für einen sicheren Texttransport
17 String verschluesseltB64 = new String( Base64.encodeBase64( verschluesselt ) );
18 System.out.println( "Verschlüsselt       " + verschluesseltB64 );
19
20 // Dekodieren
21 byte[] verschluesselt2 = Base64.decodeBase64( verschluesseltB64.getBytes() );
22
23 // Die Entschlüsselung mit dem gleichen Schlüssel
24 cipher.init(Cipher.DECRYPT_MODE, skey);
25
26 byte[] entschluesselt = cipher.doFinal(verschluesselt2);
27 System.out.println("Entschlüsselt " + new String(entschluesselt) );

Alle kryptographischen Methoden arbeiten auf der Basis von Byte-Arrays, Texte muss ein Programm daher wie in Zeile 13 konvertieren. Zur eigentlichen Verschlüsselung nutzt der Java-Entwickler bei kleinen Mengen direkt die »doFinal()« -Methode der Cipher-Klasse (Zeile 15). Für größere Mengen bietet sich das Verschlüsseln mit »CipherOutputStream« beziehungsweise »CipherInputStream« an. Der durchgeleitete Datenstrom wird dann im Stream mit dem »Cipher« -Objekt ver- oder entschlüsselt.

Möchte der Entwickler die verschlüsselten Daten wieder als Text übertragen, sind Byte-Arrays sehr unhandlich. Hier hat sich das Kodieren als Base64 bewährt. Die Bytes (-128 bis 127) werden dabei auf die in allen Systemen übertragbaren Buchstaben des englischen Alphabets abgebildet (Zeile 17). Leider enthält Java keinen Base64-Encoder im öffentlichen API, das Beispiel verwendet deshalb die Base64-Klasse aus der Apache-Commons-Codec-Bibliothek [4].

Die Entschlüsselung durchläuft die Schritte in umgekehrter Reihenfolge: Das Programm dekodiert zunächst Base64 in ein Byte-Array und entschlüsselt dieses per Cipher-Objekt. Das Cipher-Objekt muss der Java-Entwickler für beide Richtungen mit den gleichen Parametern und Schlüsseln erstellen, ansonsten schlägt die Entschlüsselung fehl.

Schlüsselfrage

Wichtig für die sichere Nutzung symmetrischer Verfahren ist die Geheimhaltung des Schlüssels. Gerät dieser in die falschen Hände, wird die Verschlüsselung nutzlos. Für den sicheren Transport bietet sich eine asymmetrische Verschlüsselung des Schlüssels an, bei Passwort-basierter Verschlüsselung reicht oft das Gedächtnis des Anwenders aus.

Das Entwickeln wirklich sicherer Verschlüsselungsalgorithmen ist ein aufwändiges Geschäft. Verschlüsselungsverfahren zu brechen ist sowohl im akademischen Umfeld wie bei den “bösen Buben” ein beliebter Sport. Vor diesem Hintergrund sollte man sich deshalb auf bekannte und gut untersuchte Algorithmen verlassen. AES und der Passwort-basierte Algorithmus decken die meisten Anwendungen ab, mit Hilfe spezieller Policy Files [5] lassen sich in Java Schlüssellängen bis zu 512 Byte nutzen. Ältere Verfahren wie DES oder Triple-DES sind hingegen mit schnellen Rechnern inzwischen leicht zu knacken und sollten nicht mehr eingesetzt werden.

Häufig soll Software sicherstellen, dass die Daten beim Übermitteln und Speichern nicht verändert wurden. Dies betrifft nicht nur absichtliche Änderungen in Dateien, auch Übertragungsfehler lassen sich so entdecken. Zu diesem Zweck kommen Hash-Algorithmen zum Einsatz, die aus beliebigen Daten einen wesentlich kleineren Fingerabdruck (Hash) berechnen. Schon kleinste Änderungen an den Daten führen zu einem anderen Fingerabdruck und lassen sich so entdecken. Der Hashwert wird dafür einmal beim Erzeuger und dann beim Nutzer der Daten berechnet, nur bei gleichen Werten sind die Daten identisch (Abbildung 3).

Hash mich

Listing 2 ermittelt einen Hash für eine ganze Datei. Die eigentliche Hash-Erzeugung findet mit dem »MessageDigest« -Objekts statt, das der Beispielcode über die Factory-Methode der Klasse erzeugt (Zeile 1). Den Hashwert berechnet ein »DigestInputStream« (Zeile 4) oder »DigestOutputStream« , der zwischen Datenquelle und -senke geschaltet ist. Dies ändert die Daten selbst nicht, aktualisiert aber das »MessageDigest« -Objekt. Von diesem ist nach abgeschlossener Übertragung der Hashwert mit der »digest()« -Methode zu erfragen (Zeile 13).

Listing 2

Berechnung eines Hash mit SHA

01 MessageDigest msgDigest = MessageDigest.getInstance("SHA-256");
02
03 InputStream fis = new FileInputStream(datei);
04 DigestInputStream dis = new DigestInputStream(fis, msgDigest);
05
06 byte[] buffer = new byte[1024];
07 int read;
08 while ((read = dis.read(buffer)) > 0) {
09    // Einlesen der ganzen Datei durch den DigestInputStream ohne sie weiter zu verwenden
10 }
11 dis.close();
12
13 byte[] digest = msgDigest.digest();
14 String digestB64 = new String(Base64.encodeBase64Chunked(digest));
15
16 System.out.println(digestB64);

Hashwerte sind Byte-Arrays und lassen sich Base64-kodiert über beliebige Kanäle übertragen. Viele Linux-Distributionen stellen zum Beispiel die Hashwerte für ihre DVD-Images auf ihren Webseiten bereit, Brennprogramme wie K3B berechnen ebenfalls Fingerabdrücke und ermöglichen es, sie zu vergleichen. Dabei sind die Hashwerte genauso schutzbedürftig wie die symmetrische Schlüssel. Ansonsten könnte ein Angreifer sowohl die übertragenen Daten als auch den Hashwert ändern und den Test sinnlos machen. Für neue Anwendungen empfiehlt sich der SHA-256- oder SHA-512-Algorithmus, die älteren MD5 und SHA-1 sind jedoch auch noch weit verbreitet.

Paarweise

Sowohl die symmetrische Verschlüsselung als auch die Hashwerte funktionieren in der Praxis nur, wenn Schlüssel oder Hashes sicher übertragen werden. Dieses Problem umgehen die asymmetrischen Verfahren, denn sie nutzen zum Ver- und Entschlüsseln unterschiedliche Teile eines Schlüsselpaars: Der öffentliche Schlüssel darf über beliebige, ungesicherte Kanäle verfügbar sein, der private Schlüssel muss sicher bei seinem Besitzer verwahrt bleiben (Abbildung 4). Für den verschlüsselten Transport werden die Daten dabei mit dem öffentlichen Schlüssel des Empfängers verschlüsselt, nur der dazu passende private Schlüssel eignet sich zur Entschlüsselung (RSA-Algorithmus).

Neben der reinen Verschlüsselung ermöglichen asymmetrische Verfahren auch digitale Signaturen (Abbildung 5). Diese errechnen sich aus dem Hashwert der Daten und dem privaten Schlüssel. Der Empfänger kann mit dem öffentlichen Schlüssel überprüfen, ob die Signatur dem Absender gehört und ob die Daten unverändert sind (RSA- und DSA-Algorithmus).

Da die asymmetrischen Verfahren vergleichsweise langsam arbeiten, kommen sie selten als Ersatz für symmetrische Verfahren in Betracht. Stattdessen werden sie für kleine Datenmengen wie den symmetrischen Schlüssel oder Hashwerte genutzt und schließen genau diese Lücke in der sicheren Übertragung. Die bekannteste Implementierung dürfte das HTTPS-Protokoll sein: Beim Aushandeln des (symmetrischen) Sitzungsschlüssels zwischen Webbrowser und Server kommen asymmetrische Verfahren zum Tragen, die eigentlichen Nutzdaten schützt dann die schnellere symmetrische Verschlüsselung.

Zertifikate

Damit sich der öffentliche Schlüssel einfach seinem Besitzer zuordnen lässt, ist er meist in ein digitales Zertifikat eingebettet. Es enthält neben dem öffentlichen Schlüssel noch weitere Angaben über den Besitzer wie Name, Organisation und E-Mail-Adresse. Zum Anfertigen und Verwalten selbst signierter Zertifikate bietet sich das Programm »keytool« an (siehe Kasten “Keytool und Jarsigner”).

Keytool und Jarsigner

Zum Java Developer Kit (JDK) gehört mit Keytool ein einfaches Programm, das Schlüsselpaare und Zertifikate erzeugt und verwaltet. Diese sind normalerweise in einer passwortgeschützten Keystore-Datei gespeichert, in der sie über einen Aliasnamen ansprechbar sind. Die folgende Kommandozeile erstellt ein Schlüsselpaar zum Verschlüsseln sowie ein selbst signiertes Zertifikat:

keytool -genkeypair -storepass noMoreSecrets -keystore rincewind.keystore -keyalg RSA -alias rincewind -dname "CN=rincewind, O=Unseen University, C=Ankh Mopork"

Der öffentliche Schlüssel lässt sich exportieren und an Kommunikationspartner senden:

keytool -exportcert -rfc -storepass noMoreSecrets -keystore rincewind.keystore -alias rincewind -file rincewind.cer

Die Partner importieren den öffentlichen Schlüssel folgendermaßen in ihren eigenen Keystore:

keytool -importcert -storepass noMoreSecrets -keystore beispiel.keystore -alias rincewind -file rincewind.cer

Den Inhalt eines Keystore zeigt die Option »-list« an:

keytool -list -v -storepass noMoreSecrets -keystore rincewind.keystore -alias rincewind

Listing 3 zeigt, wie Java-Programme Schlüssel und Zertifikate aus dem Keystore verwenden. Sind nur digitale Signaturen geplant, reicht für das Schlüsselpaar der DSA- statt des RSA-Algorithmus aus. Ebenso zum JDK gehört das Jarsigner-Programm. Es kann als Jar-Archive verpackte Java-Programme signieren. Daneben prüft es signierte Archive auf Unversehrtheit und Herkunft. Das asymmetrische Schlüsselpaar liest Jarsigner aus dem oben erzeugten Keystore. Zum Signieren gibt der Anwender neben der Jar-Datei den Aliasnamen des gewünschten Schlüsselpaars an:

jarsigner -storepass noMoreSecrets -keystore rincewind.keystore test.jar rincewind

Die »-verify« -Option dient zum Prüfen der Jar-Datei:

jarsigner -verify -verbose -storepass noMoreSecrets -keystore rincewind.keystore test.jar

Im privaten Bereich oder für Entwicklungszwecke sind die selbst signierten Zertifikate von Keytool vielleicht akzeptabel. Mehr Sicherheit verspricht aber das gemeinnützige Cacert-Project [10]. Wer hingegen Dutzende oder gar Hunderte von Anwendern mit Zertifikaten ausstattet, kommt um eine eigene Zertifikatsinfrastruktur, etwa mit EJBCA [11] umgesetzt, oder kommerzielle Anbieter nicht herum.

Unter keinen Umständen darf der privaten Schlüssel in die Händen Dritter gelangen. Geschieht dies unbemerkt, hat der Anwender keine Chance, Datenverlust oder -änderung mitzubekommen. Fällt der Schlüsseldiebstahl aber auf, kann eine funktionsfähige Zertifikatsverwaltung einzelne Zertifikate mit einer Revocation-List vor ihrem regulären Ablauf zurückrufen. Dazu müssen die Anwendungen allerdings einen Check gegen die Zertifikatsverwaltung implementieren.

Listing 3

Digitale Signatur

01 // Die Signatur
02 Signature signature = Signature.getInstance( "SHA256WITHRSA");
03
04 // Lade des  privaten Schlüssels und Zertifikats
05 char[] keystorePasswort = new char[] {'n', 'o', 'M', 'o', 'r', 'e', 'S', 'e', 'c', 'r', 'e', 't', 's'};
06 FileInputStream fis = new FileInputStream("../rincewind.keystore");
07 KeyStore keystore = KeyStore.getInstance("JKS") ;
08 keystore.load(fis, keystorePasswort);
09
10 KeyStore.PrivateKeyEntry  entry = (KeyStore.PrivateKeyEntry) keystore.getEntry("rincewind", new KeyStore.PasswordProtection(keystorePasswort));
11
12 PrivateKey privateKey = entry.getPrivateKey();
13 signature.initSign(privateKey );
14
15 // Der Inhalt
16 String klartext = "Die Botschaft";
17
18 // Signature berechnen
19 signature.update( klartext.getBytes());
20 byte[] sig = signature.sign();
21
22 String sigB64 = new String(Base64.encodeBase64(sig));
23 System.out.println( "Signatur       " +  sigB64);
24
25 // Signatur prüfen
26 Certificate cert = entry.getCertificate();
27 signature.initVerify( cert);
28 signature.update(klartext.getBytes());
29 System.out.println("Wert unverändert " + signature.verify(sig));

Listing 3 zeigt das Erstellen und Prüfen einer digitalen Signatur. Es nutzt über die Klasse »Keystore« die laut Kasten “Keytool und Jarsigner” erstellten Zertifikate. Der Aufbau des Programms ähnelt der Hashberechnung aus Listing 2, nur kommt hier ein »Signature« -Objekt zum Einsatz. Es wird mit dem passwortgeschützten privaten Schlüssel aus dem Keystore initialisiert. Zur Prüfung initialisiert das Programm das »Signature« -Objekt mit dem Zertifikat (und damit mit dem öffentlichen Schlüssel) und berechnet den Hash neu. Die Methode »verify()« prüft anschließend die übertragene Signatur. Nur wenn die Signatur sowohl zu den Daten als auch dem Zertifikat passt, gilt die Prüfung als bestanden.

Signierte Programme

Signaturen kommen bei vielen verschiedenen Datei-Arten zum Einsatz, am bekanntesten dürften signierte E-Mails, Office- [6] und PDF-Dokumente [7] sein. Eine besondere Dokumentart sind Java-Programme selbst: Mit Hilfe des Jarsigners lässt sich die Authentizität von Jar-Archiven sicherstellen. Bei Applet- oder Webstart-Anwendungen ist dies sogar Pflicht, sobald sie Zugriff auf das lokale System anfordern (Abbildung 6).

Alles gut?

Mit den in Java enthaltenen Techniken lassen sich viele Security-Probleme lösen. Von diesen einfachen Beispielen bis hin zu einem akzeptablen Sicherheitsniveau ist der Weg jedoch weit. Das fängt natürlich beim eigenen Java-Code an, daher empfiehlt sich das Studium der Secure Coding Guidelines [8]. Für Standardaufgaben wie sicheren HTTP-Transfer oder XML-Signatur [9] gibt es inzwischen fertig spezifizierte und implementierte Lösungen auf Basis der oben beschriebenen Techniken. Sie ersparen nicht nur Arbeit – die Verfahren sind mit großer Sicherheit auch besser als selbst erfundene.

Aber auch beim Einsatz bekannter Standards sollte der Entwickler recherchieren, ob nicht inzwischen Schwachstellen bekannt sind und wie sie sich beheben lassen. Daneben spielt auch die Infrastruktur eine Rolle: Symmetrische und private Schlüssel bedürfen eines wirksamen Schutzes. Zertifikate muss man vor der Verwendung auf ihre Gültigkeit prüfen, selbst die Online-Abfrage nach zurückgezogenen Zertifikaten gehört zum Java-Lieferumfang.

Neben der Anwendung ist die Implementierung der Java-VM ein viel kleineres, aber dennoch reales Problem. Erst im September 2011 wurde ein Implementationsfehler bei der Klasse Java-Applet entdeckt, der eine Hintertür bei der HTTPS-Übertragung öffnet. Außerhalb der Programmierwelt verbleibt immer noch die größte Unsicherheit: der Anwender. Sicherheit und Benutzbarkeit dürfen sich nicht gegenseitig ausschließen. Wer die Anwender jedoch mit komplizierten Prozeduren oder wüsten Passwörtern quält, muss sich über Klebezettel unter der Tastatur nicht wundern. Dann hilft auch die fortschrittlichste Kryptographie nicht mehr gegen den simplen Einbruch durch das Reinigungsteam. (mhu)

LINUX-MAGAZIN KAUFEN
EINZELNE AUSGABE Print-Ausgaben Digitale Ausgaben
ABONNEMENTS Print-Abos Digitales Abo
TABLET & SMARTPHONE APPS Readly Logo
E-Mail Benachrichtigung
Benachrichtige mich zu:
0 Kommentare
Älteste
Neuste Beste Bewertung
Inline Feedbacks
Alle Kommentare anzeigen
Nach oben