Verwenden eines benutzerdefinierten Truststores in Java sowie des Standard-Truststores


75

Ich schreibe eine Anwendung in Java, die über HTTPS eine Verbindung zu zwei Webservern herstellt. Einer hat ein Zertifikat erhalten, dem über die Standard-Vertrauenskette vertraut wird, der andere verwendet ein selbstsigniertes Zertifikat. Die Verbindung zum ersten Server funktionierte natürlich sofort, während die Verbindung zum Server mit dem selbstsignierten Zertifikat erst funktionierte, als ich einen TrustStore mit dem Zertifikat von diesem Server erstellte. Die Verbindung zum standardmäßig vertrauenswürdigen Server funktioniert jedoch nicht mehr, da anscheinend der standardmäßige TrustStore ignoriert wird, sobald ich meinen eigenen erstellt habe.

Eine Lösung, die ich gefunden habe, bestand darin, die Zertifikate aus dem Standard-TrustStore zu meinen eigenen hinzuzufügen. Diese Lösung gefällt mir jedoch nicht, da ich diesen TrustStore weiterhin verwalten muss. (Ich kann nicht davon ausgehen, dass diese Zertifikate auf absehbare Zeit statisch bleiben, oder?)

Abgesehen davon habe ich zwei 5 Jahre alte Threads mit einem ähnlichen Problem gefunden:

Registrieren mehrerer Schlüsselspeicher in JVM

Wie kann ich mehrere SSL-Zertifikate für einen Java-Server haben?

Beide gehen tief in die Java SSL-Infrastruktur ein. Ich hatte gehofft, dass es inzwischen eine bequemere Lösung gibt, die ich in einer Sicherheitsüberprüfung meines Codes leicht erklären kann.


Antworten:


91

Sie könnten ein ähnliches Muster verwenden wie in einer früheren Antwort (für ein anderes Problem).

Besorgen Sie sich im Wesentlichen den Standard-Vertrauensmanager und erstellen Sie einen zweiten Vertrauensmanager, der Ihren eigenen Vertrauensspeicher verwendet. Schließen Sie beide in eine benutzerdefinierte Trust Manager-Implementierung ein, die den Aufruf an beide delegiert (bei einem Ausfall auf den anderen zurückgreifen).

TrustManagerFactory tmf = TrustManagerFactory
    .getInstance(TrustManagerFactory.getDefaultAlgorithm());
// Using null here initialises the TMF with the default trust store.
tmf.init((KeyStore) null);

// Get hold of the default trust manager
X509TrustManager defaultTm = null;
for (TrustManager tm : tmf.getTrustManagers()) {
    if (tm instanceof X509TrustManager) {
        defaultTm = (X509TrustManager) tm;
        break;
    }
}

FileInputStream myKeys = new FileInputStream("truststore.jks");

// Do the same with your trust store this time
// Adapt how you load the keystore to your needs
KeyStore myTrustStore = KeyStore.getInstance(KeyStore.getDefaultType());
myTrustStore.load(myKeys, "password".toCharArray());

myKeys.close();

tmf = TrustManagerFactory
    .getInstance(TrustManagerFactory.getDefaultAlgorithm());
tmf.init(myTrustStore);

// Get hold of the default trust manager
X509TrustManager myTm = null;
for (TrustManager tm : tmf.getTrustManagers()) {
    if (tm instanceof X509TrustManager) {
        myTm = (X509TrustManager) tm;
        break;
    }
}

// Wrap it in your own class.
final X509TrustManager finalDefaultTm = defaultTm;
final X509TrustManager finalMyTm = myTm;
X509TrustManager customTm = new X509TrustManager() {
    @Override
    public X509Certificate[] getAcceptedIssuers() {
        // If you're planning to use client-cert auth,
        // merge results from "defaultTm" and "myTm".
        return finalDefaultTm.getAcceptedIssuers();
    }

    @Override
    public void checkServerTrusted(X509Certificate[] chain,
            String authType) throws CertificateException {
        try {
            finalMyTm.checkServerTrusted(chain, authType);
        } catch (CertificateException e) {
            // This will throw another CertificateException if this fails too.
            finalDefaultTm.checkServerTrusted(chain, authType);
        }
    }

    @Override
    public void checkClientTrusted(X509Certificate[] chain,
            String authType) throws CertificateException {
        // If you're planning to use client-cert auth,
        // do the same as checking the server.
        finalDefaultTm.checkClientTrusted(chain, authType);
    }
};


SSLContext sslContext = SSLContext.getInstance("TLS");
sslContext.init(null, new TrustManager[] { customTm }, null);

// You don't have to set this as the default context,
// it depends on the library you're using.
SSLContext.setDefault(sslContext);

Sie müssen diesen Kontext nicht als Standardkontext festlegen. Wie Sie es verwenden, hängt von der Client-Bibliothek ab, die Sie verwenden (und woher es seine Socket-Fabriken bezieht).


Grundsätzlich müssten Sie den Truststore jedoch ohnehin immer nach Bedarf aktualisieren. Das Java 7 JSSE-Referenzhandbuch enthielt einen "wichtigen Hinweis", der jetzt in Version 8 desselben Handbuchs auf nur einen "Hinweis" herabgestuft wurde :

Das JDK wird mit einer begrenzten Anzahl vertrauenswürdiger Stammzertifikate in der Datei java-home / lib / security / cacerts ausgeliefert. Wie auf den Keytool-Referenzseiten dokumentiert, liegt es in Ihrer Verantwortung, die in dieser Datei enthaltenen Zertifikate zu verwalten (dh hinzuzufügen und zu entfernen), wenn Sie diese Datei als Truststore verwenden.

Abhängig von der Zertifikatkonfiguration der Server, mit denen Sie Kontakt aufnehmen, müssen Sie möglicherweise zusätzliche Stammzertifikate hinzufügen. Beziehen Sie die erforderlichen spezifischen Stammzertifikate vom entsprechenden Anbieter.


Vielen Dank für diese großartige Lösung. Das hat mich seit ungefähr einem Jahr geplagt!
Jamie

Diese Lösung hat bei mir perfekt funktioniert. Wie beim vorherigen Kommentar habe ich viel Zeit damit verbracht, herauszufinden, wie dies ohne Erfolg zu tun ist, bis ich Ihre Lösung ausprobiert habe. Vielen Dank!
djenning90

Dieser scheint in einem Java Spring Container Env nicht zu funktionieren.
Denis Wang

@DenisWang Es hängt davon ab, was in diesem Container verwendet wird (nicht sicher, ob es sich um Spring oder Spring Boot handelt). Es besteht die Möglichkeit, dass er nicht den Standard-SSLContext verwendet, sondern einen eigenen hat. Sie müssen sich ansehen, was von dem Teil des Frühlings verwendet wird, den Sie verwenden.
Bruno

6

Sie können den Standard-Vertrauensspeicher abrufen, indem Sie aufrufen TrustManagerFactory.init((KeyStore)null)und dessen X509Certificates abrufen . Kombinieren Sie dies mit Ihrem eigenen Zertifikat. Sie können entweder das selbstsignierte Zertifikat aus einer .jksoder einer .p12Datei mit KeyStore.loadladen .crtoder eine (oder .cer) Datei über laden CertificateFactory.

Hier ist ein Demo-Code, der den Punkt veranschaulicht. Sie können den Code ausführen, wenn Sie das Zertifikat von stackoverflow.com mit Ihrem Browser herunterladen. Wenn Sie das Hinzufügen des geladenen Zertifikats und des Standardzertifikats auskommentieren SSLHandshakeException, erhält der Code ein . Wenn Sie eines der beiden beibehalten, wird der Statuscode 200 zurückgegeben.

import javax.net.ssl.*;
import java.io.*;
import java.net.URL;
import java.security.*;
import java.security.cert.*;

public class HttpsWithCustomCertificateDemo {
    public static void main(String[] args) throws Exception {
        // Create a new trust store, use getDefaultType for .jks files or "pkcs12" for .p12 files
        KeyStore trustStore = KeyStore.getInstance(KeyStore.getDefaultType());
        // Create a new trust store, use getDefaultType for .jks files or "pkcs12" for .p12 files
        trustStore.load(null, null);

        // If you comment out the following, the request will fail
        trustStore.setCertificateEntry(
                "stackoverflow",
                // To test, download the certificate from stackoverflow.com with your browser
                loadCertificate(new File("stackoverflow.crt"))
        );
        // Uncomment to following to add the installed certificates to the keystore as well
        //addDefaultRootCaCertificates(trustStore);

        SSLSocketFactory sslSocketFactory = createSslSocketFactory(trustStore);

        URL url = new URL("https://stackoverflow.com/");
        HttpsURLConnection conn = (HttpsURLConnection) url.openConnection();
        // Alternatively, to use the sslSocketFactory for all Http requests, uncomment
        //HttpsURLConnection.setDefaultSSLSocketFactory(sslSocketFactory);
        conn.setSSLSocketFactory(sslSocketFactory);
        System.out.println(conn.getResponseCode());
    }


    private static SSLSocketFactory createSslSocketFactory(KeyStore trustStore) throws GeneralSecurityException {
        TrustManagerFactory tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
        tmf.init(trustStore);
        TrustManager[] trustManagers = tmf.getTrustManagers();

        SSLContext sslContext = SSLContext.getInstance("SSL");
        sslContext.init(null, trustManagers, null);
        return sslContext.getSocketFactory();
    }

    private static X509Certificate loadCertificate(File certificateFile) throws IOException, CertificateException {
        try (FileInputStream inputStream = new FileInputStream(certificateFile)) {
            return (X509Certificate) CertificateFactory.getInstance("X509").generateCertificate(inputStream);
        }
    }

    private static void addDefaultRootCaCertificates(KeyStore trustStore) throws GeneralSecurityException {
        TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
        // Loads default Root CA certificates (generally, from JAVA_HOME/lib/cacerts)
        trustManagerFactory.init((KeyStore)null);
        for (TrustManager trustManager : trustManagerFactory.getTrustManagers()) {
            if (trustManager instanceof X509TrustManager) {
                for (X509Certificate acceptedIssuer : ((X509TrustManager) trustManager).getAcceptedIssuers()) {
                    trustStore.setCertificateEntry(acceptedIssuer.getSubjectDN().getName(), acceptedIssuer);
                }
            }
        }
    }
}

Es funktioniert nicht auf dieser Linie. trustStore.setCertificateEntry (acceptIssuer.getSubjectDN (). getName (), acceptIssuer); Es stürzt mit KeyStoreException ab: Zertifikat konnte nicht eingefügt werden; Wird der KeyStore initialisiert?
Pedro Romão

Haben Sie Anruf vergessen load(null, null). Obwohl es seltsam aussieht, ist es wichtig, da es den Keystore initialisiert 🤓
Johannes Brodwall

Ich habe das gleiche. Es ist seltsam, weil das Keystore-Objekt bereits geladen ist.
Pedro Romão

@ PedroRomão Anstatt zu versuchen, das Problem zu verstehen, habe ich das Codebeispiel durch eines ersetzt, das Sie einfach testen können. Lassen Sie mich wissen, ob es für Sie funktioniert.
Johannes Brodwall

In der Schleife zum Hinzufügen aller Zertifikate stürzt eines ab. Ich habe festgestellt, dass setCertificateEntry beim Hinzufügen eines eigenen mit dem Fehler abstürzt. Wird der Keystore initialisiert?
Pedro Romão

5

Vielleicht bin ich 6 Jahre zu spät, um diese Frage zu beantworten, aber es könnte vielleicht auch für andere Entwickler hilfreich sein. Ich hatte auch die gleiche Herausforderung, den Standard-Truststore und meinen eigenen benutzerdefinierten Truststore zu laden. Nachdem ich dieselbe benutzerdefinierte Lösung für mehrere Projekte verwendet hatte, hielt ich es für praktisch, eine Bibliothek zu erstellen und sie auch öffentlich verfügbar zu machen, um einen Beitrag zur Community zu leisten. Bitte schauen Sie hier: Github - SSLContext-Kickstart

Verwendung:

import nl.altindag.sslcontext.SSLFactory;

import javax.net.ssl.SSLContext;
import java.security.cert.X509Certificate;
import java.util.List;

public class App {

    public static void main(String[] args) {
        String trustStorePath = ...;
        char[] password = "password".toCharArray();


        SSLFactory sslFactory = SSLFactory.builder()
                .withDefaultTrustMaterial()
                .withTrustMaterial(trustStorePath, password)
                .build();

        SSLContext sslContext = sslFactory.getSslContext();
        List<X509Certificate> trustedCertificates = sslFactory.getTrustedCertificates();
    }

}

Ich war mir nicht ganz sicher, ob ich dies hier posten sollte, da es auch als ein Weg gesehen werden könnte, "meine Bibliothek" zu bewerben, aber ich dachte, es könnte für Entwickler hilfreich sein, die die gleichen Herausforderungen haben.


3

Hier ist eine sauberere Version von Brunos Antwort

public void configureTrustStore() throws NoSuchAlgorithmException, KeyManagementException, KeyStoreException,
        CertificateException, IOException {
    X509TrustManager jreTrustManager = getJreTrustManager();
    X509TrustManager myTrustManager = getMyTrustManager();

    X509TrustManager mergedTrustManager = createMergedTrustManager(jreTrustManager, myTrustManager);
    setSystemTrustManager(mergedTrustManager);
}

private X509TrustManager getJreTrustManager() throws NoSuchAlgorithmException, KeyStoreException {
    return findDefaultTrustManager(null);
}

private X509TrustManager getMyTrustManager() throws FileNotFoundException, KeyStoreException, IOException,
        NoSuchAlgorithmException, CertificateException {
    // Adapt to load your keystore
    try (FileInputStream myKeys = new FileInputStream("truststore.jks")) {
        KeyStore myTrustStore = KeyStore.getInstance("jks");
        myTrustStore.load(myKeys, "password".toCharArray());

        return findDefaultTrustManager(myTrustStore);
    }
}

private X509TrustManager findDefaultTrustManager(KeyStore keyStore)
        throws NoSuchAlgorithmException, KeyStoreException {
    TrustManagerFactory tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
    tmf.init(keyStore); // If keyStore is null, tmf will be initialized with the default trust store

    for (TrustManager tm : tmf.getTrustManagers()) {
        if (tm instanceof X509TrustManager) {
            return (X509TrustManager) tm;
        }
    }
    return null;
}

private X509TrustManager createMergedTrustManager(X509TrustManager jreTrustManager,
        X509TrustManager customTrustManager) {
    return new X509TrustManager() {
        @Override
        public X509Certificate[] getAcceptedIssuers() {
            // If you're planning to use client-cert auth,
            // merge results from "defaultTm" and "myTm".
            return jreTrustManager.getAcceptedIssuers();
        }

        @Override
        public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException {
            try {
                customTrustManager.checkServerTrusted(chain, authType);
            } catch (CertificateException e) {
                // This will throw another CertificateException if this fails too.
                jreTrustManager.checkServerTrusted(chain, authType);
            }
        }

        @Override
        public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException {
            // If you're planning to use client-cert auth,
            // do the same as checking the server.
            jreTrustManager.checkClientTrusted(chain, authType);
        }

    };
}

private void setSystemTrustManager(X509TrustManager mergedTrustManager)
        throws NoSuchAlgorithmException, KeyManagementException {
    SSLContext sslContext = SSLContext.getInstance("TLS");
    sslContext.init(null, new TrustManager[] { mergedTrustManager }, null);

    // You don't have to set this as the default context,
    // it depends on the library you're using.
    SSLContext.setDefault(sslContext);
}

-2

Wie ich herausgefunden habe, können Sie auch eine SSLContextBuilderKlasse aus der Apache HttpComponents-Bibliothek verwenden, um Ihren benutzerdefinierten Schlüsselspeicher zu einem hinzuzufügen SSLContext:

SSLContextBuilder builder = new SSLContextBuilder();
try {
     keyStore.load(null, null);
     builder.loadTrustMaterial(keyStore, null);
     builder.loadKeyMaterial(keyStore, null);
} catch (NoSuchAlgorithmException | KeyStoreException | CertificateException | IOException
          | UnrecoverableKeyException e) {
     log.error("Can not load keys from keystore '{}'", keyStore.getProvider(), e);
}
return builder.build();

Geht es nicht darum, das gesamte Vertrauensmaterial in einem einzigen Vertrauensspeicher hinzuzufügen? SSLContextBuilder#loadTrustMaterialkann verwendet werden, um nur einen Truststore zu laden. Die andere Methode ist SSLContextBuilder#loadKeyMaterialnicht für den Truststore, sondern für den Keystore, der einem anderen Zweck dient. Ist es nicht?
Ramtech

Nein, ich denke, loadTrustMaterial () lädt die TrustManager aus den angegebenen Parametern und fügt sie intern einer Liste hinzu. Sie können es also mehrmals aufrufen und die TrustManager werden aggregiert.
OliLay

1
Ich schaue mir an, was passieren loadTrustMaterialwird. Zumindest in der Java11-Implementierung wird die Liste durch die angegebene Liste ersetzt. Wenn Sie dekompilieren sun.security.provider.JavaKeyStore#engineLoad, werden Sie this.entries.clear();dort sehen.
m Lotfizad
Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.