Подтвердить что ты не робот

Java SSL подключиться, добавить серверный сертификат в хранилище ключей программно

Я подключаю SSL-клиент к моему серверу SSL. Когда клиент не проверяет сертификат из-за корня, который не существует в хранилище ключей клиента, мне нужно добавить этот сертификат в хранилище локального ключа в коде и продолжить. Существуют примеры для того, чтобы всегда принимать все сертификаты, но я хочу, чтобы пользователь проверил сертификат и добавил его в хранилище локального ключа, не выходя из приложения.

SSLSocketFactory sslsocketfactory = (SSLSocketFactory) SSLSocketFactory.getDefault();
SSLSocket sslsocket = (SSLSocket) sslsocketfactory.createSocket("localhost", 23467);
try{
    sslsocket.startHandshake();
} catch (IOException e) {
    //here I want to get the peer certificate, conditionally add to local key store, then reauthenticate successfully
}

Есть много вещей о пользовательских SocketFactory, TrustManager, SSLContext и т.д., и я действительно не понимаю, как они все подходят друг к другу или который будет самым коротким путем для моей цели.

4b9b3361

Ответ 1

Это можно реализовать с помощью X509TrustManager.

Получить SSLContext с помощью

SSLContext ctx = SSLContext.getInstance("TLS");

Затем инициализируйте его своим пользовательским X509TrustManager, используя SSLContext # init. SecureRandom и KeyManager [] могут быть пустыми. Последнее полезно, только если вы выполняете аутентификацию клиента, если в вашем сценарии только серверу необходимо пройти аутентификацию, вам не нужно его устанавливать.

Из этого SSLContext получите ваш SSLSocketFactory, используя SSLContext # getSocketFactory и продолжайте, как планировалось.

Что касается реализации X509TrustManager, это может выглядеть так:

TrustManager tm = new X509TrustManager() {
    public void checkClientTrusted(X509Certificate[] chain,
                    String authType)
                    throws CertificateException {
        //do nothing, you're the client
    }

    public X509Certificate[] getAcceptedIssuers() {
        //also only relevant for servers
    }

    public void checkServerTrusted(X509Certificate[] chain,
                    String authType)
                    throws CertificateException {
        /* chain[chain.length -1] is the candidate for the
         * root certificate. 
         * Look it up to see whether it in your list.
         * If not, ask the user for permission to add it.
         * If not granted, reject.
         * Validate the chain using CertPathValidator and 
         * your list of trusted roots.
         */
    }
};

Изменить:

Райан был прав, я забыл объяснить, как добавить новый корень в существующие. Предположим, что ваш текущий KeyStore доверенных корней был получен из cacerts ( "хранилище доверия по умолчанию Java", которое поставляется вместе с вашим JDK, расположенным под jre/lib/security). Я предполагаю, что вы загрузили это хранилище ключей (в формате JKS) с загрузкой KeyStore # (InputStream, char []).

KeyStore ks = KeyStore.getInstance("JKS");
FileInputStream in = new FileInputStream("<path to cacerts"");
ks.load(in, "changeit".toCharArray);

Пароль по умолчанию для cacerts является "changeit", если вы этого не сделали, ну, изменив его.

Затем вы можете добавить дополнительные доверительные корни, используя KeyStore # setEntry. Вы можете опустить ProtectionParameter (т.е. Null), KeyStore.Entry будет TrustedCertificateEntry, который добавляет новый root в качестве параметра в свой конструктор.

KeyStore.Entry newEntry = new KeyStore.TrustedCertificateEntry(newRoot);
ks.setEntry("someAlias", newEntry, null);

Если вы захотите в какой-то момент сохранить измененное хранилище доверия, вы можете достичь этого с помощью хранилища KeyStore # (OutputStream, char [].

Ответ 2

В JSSE API (часть, которая заботится о SSL/TLS) проверка того, доверяет ли сертификат, не обязательно включает KeyStore. Это будет иметь место в подавляющем большинстве случаев, но при условии, что всегда будет один неверный. Это делается через TrustManager. Этот TrustManager, скорее всего, использует хранилище доверия по умолчанию KeyStore или тот, который указан с помощью системного свойства javax.net.ssl.trustStore, но это необязательно, и этот файл не обязательно $JAVA_HOME/jre/lib/security/cacerts (все это зависит от JRE).

В общем случае вы не можете получить KeyStore, который используется диспетчером доверия, который вы используете из приложения, тем более, если хранилище доверия по умолчанию используется без каких-либо параметров системного имущества. Даже если бы вы смогли узнать, что это за KeyStore, вы все равно столкнетесь с двумя возможными проблемами (по крайней мере):

  • Возможно, вы не сможете записать этот файл (что не обязательно является проблемой, если вы не сохраняете изменения навсегда).
  • Этот KeyStore может быть даже не основан на файлах (например, это может быть Keychain на OSX), и у вас может не быть доступа для записи.

Я бы предложил написать обертку по умолчанию TrustManager (более конкретно, X509TrustManager), которая выполняет проверки против диспетчера доверия по умолчанию и, если эта первоначальная проверка не выполняется, выполняет обратный вызов для пользователя, интерфейс, чтобы проверить, следует ли добавить его в "локальное" хранилище доверия.

Это можно сделать, как показано в в этом примереbrief unit test), если вы хотите использовать что-то вроде jSSLutils.

Подготовка вашего SSLContext будет выполнена следующим образом:

KeyStore keyStore = // ... Create and/or load a keystore from a file
                    // if you want it to persist, null otherwise.

// In ServerCallbackWrappingTrustManager.CheckServerTrustedCallback
CheckServerTrustedCallback callback = new CheckServerTrustedCallback {
    public boolean checkServerTrusted(X509Certificate[] chain,
            String authType) {
        return true; // only if the user wants to accept it.
    }
}

// Without arguments, uses the default key managers and trust managers.
PKIXSSLContextFactory sslContextFactory = new PKIXSSLContextFactory();
sslContextFactory
   .setTrustManagerWrapper(new ServerCallbackWrappingTrustManager.Wrapper(
                    callback, keyStore));
SSLContext sslContext = sslContextFactory.buildSSLContext();
SSLSocketFactory sslSocketFactory = sslContext.getSslSocketFactory();
// ...

// Use keyStore.store(...) if you want to save the resulting keystore for later use.

(Разумеется, вам не нужно использовать эту библиотеку и ее SSLContextFactory, но, по вашему усмотрению, используйте собственные X509TrustManager, "wrapping" или not default.)

Другим фактором, который вы должны учитывать, является взаимодействие с этим обратным вызовом. К моменту, когда пользователь принял решение нажать "принять" или "отклонить" (например), квитирование может иметь тайм-аут, поэтому вам может потребоваться вторая попытка подключения, когда пользователь принял сертификат.

Еще один момент, который следует учитывать при разработке обратного вызова, заключается в том, что диспетчер доверия не знает, какой сокет используется (в отличие от его аналога X509KeyManager), поэтому должна быть такая небольшая двусмысленность в отношении того, какое действие пользователя вызвало это всплывающее окно (или, тем не менее, вы хотите реализовать обратный вызов). Если выполняется несколько подключений, вы не захотите проверять неверный. Кажется возможным решить эту проблему, используя отдельный обратный вызов, SSLContext и SSLSocketFactory для SSLSocket, который должен был создать новое соединение, каким-то образом связать SSLSocket и обратный вызов с действием, предпринятым пользователем для запуска этой попытки подключения в первом место.