#

HttpClient und SSL

Gerade hatte ich mit dem Apache HttpClient und einer SSL Verbindung zu kämpfen. Damit anderen bei solchen Problemen auch geholfen ist, hier ein paar Tips.

Zu erst benötigt man einen Keystore, welcher den Private- und den Publickey für die verschlüsselte Kommunikation enthält. Hierfür wird das im JDk enthaltene „keytool“ benutzt. Mit

keytool -genkey -alias tomcat -keyalg RSA

wird die Datei „.keystore“ im „Dokumente und Einstellungen“ Verzeichnis des aktuellen Benutzers erzeugt. Wie man sehen kann ist es sinnvoll, den alias tomcat sowie das Passwort „changeit“ zu benutzen, damit Tomcat diesen Keystore für eingehende HTTPS Verbindungen benutzen kann. Es läßt sich aber auch ein anderes Passwort definieren, welches dann in der server.xml angegeben werden muss.

Der zweite Schritt besteht darin, das Zertifikat der Zielseite zu speichern. Das einfachste Vorgehen hierfür ist unter dem IE das Zertifikat anzeigenlassen und unter Details im Format „DER-kodiert-binär X.509 (CER)“ zu exportieren. im folgenden nehmem wir an, das Zertifikat wurde unter dem Dateinamen „target.cer“ gespeichert.

Nun muss das Zielzertifikat in einen Truststore eingebunden werden, welcher alle von Java als vertrauenswürdig erachteten Zertifikate enthält. Unter JSDK\jre\lib\security\cacerts liegen die Default-Zertifikate, welche automatisch akzeptiert werden. Hierfür muss aber die Zielseite über eine von einer akzeptierten CA stelle zertifiziertes Zertifikat verfügen, was nicht immer der Fall ist. Mit

keytool -import -alias targetcert -file target.cer 
  -keystore C:\Dokumente und Einstellungen\me\truststore

kann ein Truststore erzeugt werden, welches das eben gespeicherte Zertifikat enthält. Wo man das ganze hinspeichert ist eigentlich egal, wird aber später noch benötigt.

Im Javacode muss nun nur noch der Truststore in den Systemproperties angegeben werden:

System.setProperty("javax.net.ssl.trustStore",
   "C:\\Dokumente und Einstellungen\\me\\truststore");
System.setProperty("javax.net.ssl.trustStorePassword", 
  "changeit");

Wobei das Passwort für den Truststore nicht zwingend ist.
Damit ist das ganze schon fertig. Nun kann man u. a. den HttpClient von Apache benutzen, um sich mit der Zielseite vertrauenswürdigerweise zu verbinden.

Übrigens gibt es die Meldung „sun.security.validator.ValidatorException: No trusted certificate found“, wenn man das Zielzertifikat nicht wie oben beschrieben einbindet.

Nachtrag vom 24.03.2006@13:54

Das war aber noch nicht alles. Wenn man einen HttpClient benutzt und das Problem Java-intern lösen möchte, ohne über die Shell das keytool zu benutzen, gibt es auch weitere Möglichkeiten.
Die unsauberste Möglichkeit ist es, seinen Client alle Zertifikate akzeptieren zu lassen. Damit ist aber der Sinn der Zertifikation komplett ausgehebelt. Die Verbindung ist zwar immernoch Verschlüsselt, die Authentifikation des Zielservers jedoch nicht gegeben. Um alle Zertifikate zu akzeptieren, kann man einfach den unter commons/httpclient/contrib/ssl aufgeführte EasySSLProtocolSocketFactory.java Klasse benutzen.
Bei der Zertifikat prüfenden Variante AuthSSLProtocolSocketFactory.java benötigt man bereits einen Keystore und einen Truststore.
Ganz ohne die Stores kommt man aber auch aus, wenn man sich seinen eigenen TrustManager bastelt:

public class SingleCertificateAuthSSLX509TrustManager 
  implements X509TrustManager{
 
  private X509Certificate localCert;
  private X509TrustManager standardTrustManager = null;
 
  public SingleCertificateAuthSSLX509TrustManager(
    X509Certificate certificate)
    throws NoSuchAlgorithmException,
    KeyStoreException {
 
    super();
    localCert = certificate;
    TrustManagerFactory trustfactory = 
      TrustManagerFactory.getInstance(
        TrustManagerFactory.getDefaultAlgorithm());
    KeyStore ks = null;
    trustfactory.init(ks);
    TrustManager[] trustmanagers = trustfactory.getTrustManagers();
    if (trustmanagers.length == 0) {
      throw new NoSuchAlgorithmException("no trust manager found");
    }
    standardTrustManager = (X509TrustManager) trustmanagers[0];
  }
 
  public void checkClientTrusted(
    X509Certificate[] certificates,
    String authType) 
    throws CertificateException {
 
    standardTrustManager.checkClientTrusted(certificates, authType);
  }
 
  public void checkServerTrusted(
    X509Certificate[] certificates,
    String authType) 
    throws CertificateException {
 
    if (certificates != null) {
      boolean foundEqual = false;
      X509Certificate cert;
      Iterator iter;
      for (int c = 0; c<certificates.length; c++) {
        cert = certificates[c];
        cert.checkValidity();
        if (localCert.equals(cert)) {
          foundEqual = true;
          break;
        }
      }
      if (foundEqual) return;
    }
    throw new CertificateException("no corresponding certificate found");
  }
 
  public X509Certificate[] getAcceptedIssuers() {
    return standardTrustManager.getAcceptedIssuers();
  }
}

Im Konstruktor erwartet dieser TrustManager ein Zertifikat, gegen welches er das Zielserverzertifikat prüft. Es werden nur Zertifikate von Servern geprüft, auf welche der HttpClient sich verbindet. Eingehende Verbindungen (,welche beim HttpClient sowieso nicht benötigt werden,) werden an den Default TrustManager deligiert.

Die Server-Zertifikate können im IE Base64 kodiert exportiert werden und liegen dann als Text vor. Dieser Text kann u.a. in Datenbanken oder Konfigurationsdateien abgelegt werden. Mit Hilfe eines Base64 De- / Encoders und der folgenden Methode können diese Base64 kodierten Zertifikate in X509Certificate Objekte umgewandelt werden:

private static final String BASE64_CERT_HEADER = 
  "-----BEGIN CERTIFICATE-----";
private static final String BASE64_CERT_FOOTER = 
  "-----END CERTIFICATE-----";
 
  public static final X509Certificate convertBase64ToX509Certificate(
    String certificate) 
    throws CertificateException, NoSuchProviderException {
 
    CertificateFactory certfactory = 
      CertificateFactory.getInstance("X.509", "BC");
    if (certificate.startsWith(BASE64_CERT_HEADER)) {
      certificate = 
        certificate.substring(BASE64_CERT_HEADER.length());
    }
    if (certificate.endsWith(BASE64_CERT_FOOTER)) {
      certificate = 
        certificate.substring(0, certificate.length()
          - BASE64_CERT_FOOTER.length());
    }
 
    byte[] decBytes = Base64.decode(certificate);
 
    Collection localCerts = 
      certfactory.generateCertificates(
        new ByteArrayInputStream(decBytes));
    if (localCerts.isEmpty()) return null;
    else return (X509Certificate) localCerts.iterator().next();
  }

Hierbei werden die Header- und Footerzeile des Zertifikates entfernt und der Base64 enkodierte Teil dekodiert.
Somit bleibt man flexibel und kann ohne Key-/Truststore arbeiten.

Tags:, , , ,

Leave a Reply »»

Note: All comments are manually approved to avoid spam. So if your comment doesn't appear immediately, that's ok. Have patience, it can take some days until I have the time to approve my comments.