FIRST : Das hier soll eher so eine Art kleines Tutorial / kleine Hilfestellung sein, keine Frage.
Ach ja, wer kennt es nicht : FREE SSL-Certs von StartSSL.com … aber nicht mit Java.
Warum eigentlich nicht? Im Browser funktionierts doch auch ohne Probleme.
Tja, das liegt daran das Java seine eigene CA-Verwaltung hat, also ein eigenes Set an als „vertrauenswürdig“ eingestuften Root-Zertifikaten die als Basis für die Bildung der Zertifikats-Ketten genutzt werden. Und in eben dieser Liste ist das StartSSL.com - Root-CA - Zertifikat nicht eingetragen. Man kann jetzt drüber spekulieren ob es vielleicht einfach daran liegt das Oracle als US-Unternehmen einer kleinen Hacker-Werkstatt aus Israel nicht traut, oder ob es irgendwelche US-Gesetzte diesbezüglich gibt (Dürfte es eigentlich nicht geben, sonst würde Microsoft den Internet Explorer auch nicht mit diesem Zertifikat ausstatten.), oder was oder wer auch immer sonst dafür verantwortlich ist. Fakt ist : so out-of-the-box rennt man bei StartSSL-Zertifikaten in ziemliche Probleme … oder ?
Und genau die Frage hat sich mir auch gestellt : Es gibt zwar Möglichkeiten über „import in den KeyStore“ oder „trust-all Implementierung“, aber das kann doch nicht die Lösung sein !
Also habe ich Google nach einer Möglichkeit gefragt wie man denn nun ein vorhandenes Root-Zertifikat zur Runtime on-the-fly „nachladen“ kann, und bin dabei auf folgendes gestoßen : java - Implementing X509TrustManager - passing on part of the verification to existing verifier - Stack Overflow
Im verlinkten Thema geht es um die Frage wie man einen TrustManager implementieren kann der zwar keine „trust-all Implementierung“, aber auch nicht auf den KeyStore beschränkt ist sondern mit eigenen Zertifikaten erweitert werden kann.
Mit Hilfe des Beispielcodes habe ich mich dann durch die API-Doc gewurstet um erstmal rauszubekommen ob, und wenn ja, wie man zur Runtime Zertifikate „nachladen“ kann.
Es ging erstmal damit los zu ergründen wie überhaupt eine HttpsURLConnection aufgebaut wird. Dabei wird intern auf javax.net.ssl.SSLSocket zurückgegriffen, welcher wiederum über die javax.net.SSLSocketFactory erzeugt werden kann. Gut, ein Ansatzpunkt, denn HttpsURLConnection bietet mit setSSLSocketFactory(SSLSocketFactory) die Möglichkeit eine eigene Factory zu setzen bevor die eigentliche Verbindung mit connect() hergestellt wird.
Der nächste Punkt war nun zu verstehen wie ich eine SSLSocketFactory mit dem von mir gewünschten Root-Zertifikat erzeuge ohne selbst irgendwas mit extends erweitern zu müssen und dabei möglicherweise Fehler mache. An eine SSLSocketFactory kommt man über zwei Wege : SSLSocketFactory.getDefault(), wie der Name schon sagt nicht das was ich suche … und SSLContext.getSocketFactory().
Ok, von SSLContext habe ich schon mal was im Netz gelesen, und dazu findet man auch so einiges. Also weiter : Wie baue ich mir eine SSLContext-Objekt mit dem gewünschten Zertifikat ?
Liest man sich den ganzen Krams durch den Google liefert fällt immer wieder : TrustManager. Und siehe da : SSLContext.init(KeyManager, TrustManager, SecureRandom).
Nun war ich aber wieder genau am Anfang meiner Suche : TrustManager. Und alles was Google dazu so liefert läuft fast immer wieder nur auf eine selbst-Implementierung mit extends hinaus. Aber genau das wollte ich doch nicht. DERP.
Doch dann viel mir plötzlich der Code von StackOverflow wieder ein : TrustManagerFactory ! Aha, es gibt also einen Weg sich einen TrustManager durch eine Factory erstellen zu lassen. DAS muss es doch jetzt aber sein. Und ja, das ist auch der richtige Weg. Also, auf die Doc der Factory : TrustManagerFactory.init(KeyStore).
WAS IS ? Wo soll ich denn jetzt einen KeyStore herbekommen ? Ob ich mir das Root-CA runterlade und in den default-KeyStore packe oder in eine eigene Datei und die dann lade, macht doch keinen Unterschied (höchstens den das man an den „System“-KeyStore ohne Adminrechte nicht rankommt).
Aber naja, jetzt hab ich mich schon so wild durch die Doc geklickt, da kann ich jetzt auch noch in die Page gucken.
Und ach, gucke mal da, was springt mir denn da ins Auge :
KeyStore.TrustedCertificateEntry
This type of entry contains a single public key Certificate belonging to another party. It is called a trusted certificate because the keystore owner trusts that the public key in the certificate indeed belongs to the identity identified by the subject (owner) of the certificate.
This type of entry can be used to authenticate other parties.
Wenn das schon so beschrieben ist muss es doch einen Weg geben ein KeyStore im RAM zu erzeugen und dort das gewünschte Zertifikat zu hinterlegen. So schwer kanns doch echt nicht sein. Ob nun mit einem File oder ein paar Bytes im RAM, für die VM ist es letzten Endes gleich woher die Daten kommen.
Also hatte ich dann mal eben schnell folgendes Ausprobiert :
X509Certificate x509cert=(X509Certificate)factory.generateCertificate(new FileInputStream(new File("ca.crt")));
KeyStore keyStore=KeyStore.getInstance(KeyStore.getDefaultType());
keyStore.setCertificateEntry("StartSSL", x509cert);
TrustManagerFactory tmFactory=TrustManagerFactory.getInstance("X509");
tmFactory.init(keyStore);```
Zwei kleine Probleme gab es dann aber doch noch :
1) Ich musste erstmal rausbekommen welches Format nun eigentlich von der Factory genutzt werden kann, in der Doc steht was von nem Base64-encoded Type. Den gibt es so aber nicht direkt vom StartSSL-Server. Und da mir der Unterschied zwischen den unterschiedlichen Typen nicht bekannt und auch eigentlich ziemlich egal ist hab ich halt nach der Reihe ausprobiert. Man braucht für die x509-Factory das DER Format (welches sowohl im CRT als auch CER steckt, keine Ahnung was der Unterschied ist).
2) Kaum hatte ich es geschafft das Zertifikat ohne Fehler zu laden kam beim Verbindungsaufbau die nächste Expeption : KeyStore must be initialized!
HÄ ? Wat ? Also noch mal in die Doc !
...
Ach ja, da stehts ja auch :
> Before a keystore can be accessed, it must be loaded.
Und wie macht man das ? Gut das "loaded" als Hyperlink ausgelegt ist > draufklicken > und bei KeyStore.load(InputStream, char[]) landen.
In dem Moment hab ich erlich gesagt so leicht die Hoffnung verloren weil ich dachte : na toll, alles umsonst, musst doch mit nem File arbeiten. Aber dies mal hab ich zum Glück weiter gelesen :
> In order to create an empty keystore, or if the keystore cannot be initialized from a stream, pass null as the stream argument.
JA ! ENDLICH ! Die Suche hat ein Ende.
Also eingebaut : LÄUFT !
Mein Gotte, was für ein ewiges rumgeklicke. Aber letzten Endes hat es doch zum Erfolg geführt : ich war in der Lage mit Hilfe des geladenen Zertifikats und den ganzen Factories eine Socket zu erzeugen dessen SSL-Handshake das geladene StartSSL-Root-CA-Zertifikat zur Prüfung genutzt hat und konnte erfolgreich eine TLSv1.2 Verbindung herstellen.
Ich hatte dann noch die Idee : was machen wenn das StartSSL-Zertifikat nicht verfügbar ist ? Na klar, einfach runterladen. Denn zum Glück kann man das CA.crt auch über eine normale HTTP-Verbindung laden.
Tja, und als ich so diese Zeilen schreibe fällt mir ein : "Was für ein absoluter schwachsinn ! Wenn man sich schon die Mühe macht legt man sicher das CRT halt mit bei."
Rausgekommen ist dann am Ende das hier :
```package ssl;
// mal wieder großzügiges import, wenn man halt mal eben ohne IDE bastelt ...
import java.io.*;
import java.net.*;
import javax.net.ssl.*;
import java.security.*;
import java.security.cert.*;
public class Test
{
public static void main(String[] args) throws Exception
{
CertificateFactory factory=CertificateFactory.getInstance("X.509");
X509Certificate x509cert=(X509Certificate)factory.generateCertificate(Thread.currentThread().getContextClassLoader().getResourceAsStream("ssl/startssl_ca.crt"));
KeyStore keyStore=KeyStore.getInstance(KeyStore.getDefaultType()); // laut Java-Doc wird, wenn nicht anders definiert, "jks" geliefert, könnte man also auch hard-coden
keyStore.load(null, null);
keyStore.setCertificateEntry("StartSSL", x509cert); // Alias-Name kann frei gewählt werden
TrustManagerFactory tmFactory=TrustManagerFactory.getInstance("X509");
tmFactory.init(keyStore);
SSLContext context=SSLContext.getInstance("TLSv1.2"); // ggf. nur getInstance("TLS"); kommt auf den Server an
context.init(null, tmFactory.getTrustManagers(), null); // erster Parameter KeyManager nur für Client-Auth wichtig / wenn SecureRandom == null wird (wie bei allen Crypto-Klassen) der "höchstwertigste" installierte Provider genutzt
SSLSocketFactory socketFactory=context.getSocketFactory();
URL url=new URL("https://www.startssl.com");
HttpsURLConnection con=(HttpsURLConnection)url.openConnection();
con.setSSLSocketFactory(socketFactory);
con.connect();
BufferedReader in=new BufferedReader(new InputStreamReader(con.getInputStream()));
String line="";
while((line=in.readLine())!=null)
{
System.out.println(line);
}
in.close();
con.disconnect();
}
}```
Sicher, erstmal als Test nur das Laden der Hauptseite. Aber als proof-of-concept durch aus ausreichend.
Das ist also der Weg wenn man "sauber" das StartSSL.com Root-CA Zertifikat zur Runtime laden will um eine Verbindung zu einem Server aufzubauen der ein von StartSSL signiertes Zertifikat nutzt.
Ist zwar etwas umständlich und um 100 Ecken und durch 1.000 Factories, aber irgendwie immer noch besser als irgendwas mit extends TrustManager zu basteln und dabei das ganze Sicherheitskonzept was dahinter steht doch wieder auszuhebeln.
Ich hoffe es ist dem einen oder anderen eine Hilfe bezüglich dieses Problems. Vielleicht hat ja noch jemand weitere Ideen ... immer her damit.
Ansonsten wars das erstmal wieder von mir soweit bis hier hin ...
euer Schnitzel
over and out