Поддерживает ли Java возможность шифрования сертификатов?

Я разрабатываю Java-приложение, которое общается с удаленным сервером через HTTP.

Несколько месяцев назад я попытался переключиться на HTTPS, установив на сервер сертификат StartSSL. Общие браузеры, такие как Chrome или Firefox, приняли сертификат, но мое приложение отклонило соединение, поскольку StartSSL не находится в списке CA по умолчанию. Наконец, я вернулся к использованию HTTP.

Теперь, когда Let encrypt начал публичную бета-версию, я хотел бы знать, работает ли в настоящее время Java (или подтверждено, что она работает в будущее) со своими сертификатами по умолчанию.

Пусть Encrypt получил промежуточный кросс-подписанный IdenTrust, который должен быть хорошей новостью. Однако я не могу найти ни одного из этих двух в выводе этой команды:

keytool -keystore "..\lib\security\cacerts" -storepass changeit -list

Я знаю, что доверенные ЦС могут быть добавлены вручную на каждой машине, но поскольку мое приложение должно быть бесплатным для загрузки и выполнения без дополнительной настройки, я ищу решения, которые работают "из коробки". У вас есть хорошие новости для меня?

Ответ 1

[ Обновление 2016-06-08: согласно https://bugs.openjdk.java.net/browse/JDK-8154757, центр IdenTrust будет включен в Oracle Java 8u101.]

[ Обновление 2016-08-05: Java 8u101 был выпущен и действительно включает в себя IdenTrust CA: примечания к выпуску]


Поддерживает ли Java возможность шифрования сертификатов?

Да. Сертификат Let Encrypt - это просто обычный сертификат открытого ключа. Java поддерживает его (согласно Разрешить шифрование совместимости сертификатов, для Java 7 >= 7u111 и Java 8 >= 8u101).

Предоставляет ли Java доверие возможность шифрования сертификатов из коробки?

Нет/это зависит от JVM. Трастовая база Oracle JDK/JRE до 8u66 не содержит ни CA, ни шифрования, ни CAI, ни IDETrust, которые подписывали ее. new URL("https://letsencrypt.org/").openConnection().connect(); например, приводит к javax.net.ssl.SSLHandshakeException: sun.security.validator.ValidatorException.

Однако вы можете предоставить свой собственный валидатор/определить настраиваемое хранилище ключей, которое содержит требуемый корневой ЦС, или импортировать сертификат в доверительный магазин JVM.

https://community.letsencrypt.org/t/will-the-cross-root-cover-trust-by-the-default-list-in-the-jdk-jre/134/10 также обсуждает эту тему.


Вот пример кода, который показывает, как добавить сертификат в хранилище по умолчанию во время выполнения. Вам просто нужно добавить сертификат (экспортированный из firefox как .der и put в classpath)

На основе Как получить список доверенных корневых сертификатов в Java? и http://developer.android.com/training/articles/security-ssl.html#UnknownCa

import java.io.BufferedInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.net.URLConnection;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.security.KeyStore;
import java.security.cert.Certificate;
import java.security.cert.CertificateFactory;
import java.security.cert.PKIXParameters;
import java.security.cert.TrustAnchor;
import java.security.cert.X509Certificate;

import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLHandshakeException;
import javax.net.ssl.TrustManagerFactory;

public class SSLExample {
    // BEGIN ------- ADDME
    static {
        try {
            KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType());
            Path ksPath = Paths.get(System.getProperty("java.home"),
                    "lib", "security", "cacerts");
            keyStore.load(Files.newInputStream(ksPath),
                    "changeit".toCharArray());

            CertificateFactory cf = CertificateFactory.getInstance("X.509");
            try (InputStream caInput = new BufferedInputStream(
                    // this files is shipped with the application
                    SSLExample.class.getResourceAsStream("DSTRootCAX3.der"))) {
                Certificate crt = cf.generateCertificate(caInput);
                System.out.println("Added Cert for " + ((X509Certificate) crt)
                        .getSubjectDN());

                keyStore.setCertificateEntry("DSTRootCAX3", crt);
            }

            if (false) { // enable to see
                System.out.println("Truststore now trusting: ");
                PKIXParameters params = new PKIXParameters(keyStore);
                params.getTrustAnchors().stream()
                        .map(TrustAnchor::getTrustedCert)
                        .map(X509Certificate::getSubjectDN)
                        .forEach(System.out::println);
                System.out.println();
            }

            TrustManagerFactory tmf = TrustManagerFactory
                    .getInstance(TrustManagerFactory.getDefaultAlgorithm());
            tmf.init(keyStore);
            SSLContext sslContext = SSLContext.getInstance("TLS");
            sslContext.init(null, tmf.getTrustManagers(), null);
            SSLContext.setDefault(sslContext);
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }
    // END ---------- ADDME

    public static void main(String[] args) throws IOException {
        // signed by default trusted CAs.
        testUrl(new URL("https://google.com"));
        testUrl(new URL("https://www.thawte.com"));

        // signed by letsencrypt
        testUrl(new URL("https://helloworld.letsencrypt.org"));
        // signed by LE cross-sign CA
        testUrl(new URL("https://letsencrypt.org"));
        // expired
        testUrl(new URL("https://tv.eurosport.com/"));
        // self-signed
        testUrl(new URL("https://www.pcwebshop.co.uk/"));

    }

    static void testUrl(URL url) throws IOException {
        URLConnection connection = url.openConnection();
        try {
            connection.connect();
            System.out.println("Headers of " + url + " => "
                    + connection.getHeaderFields());
        } catch (SSLHandshakeException e) {
            System.out.println("Untrusted: " + url);
        }
    }

}

Ответ 2

Я знаю, что OP запросил решение без локальных изменений конфигурации, но если вы хотите постоянно добавить цепочку доверия в хранилище ключей:

$ keytool -trustcacerts \
    -keystore $JAVA_HOME/jre/lib/security/cacerts \
    -storepass changeit \
    -noprompt \
    -importcert \
    -file /etc/letsencrypt/live/hostname.com/chain.pem

источник: https://community.letsencrypt.org/t/will-the-cross-root-cover-trust-by-the-default-list-in-the-jdk-jre/134/13

Ответ 3

Подробный ответ для тех из нас, кто хочет внести локальные изменения конфигурации, включая резервное копирование файла конфигурации:

1. Проверьте, работает ли он перед изменениями

Если у вас уже нет тестовой программы, вы можете использовать мою java-программу SSLPing ping, которая проверяет рукопожатие TLS (будет работать с любым портом SSL/TLS, а не только с HTTPS). Я буду использовать предварительно построенный SSLPing.jar, но чтение кода и его создание - задача быстро и легко:

$ git clone https://github.com/dimalinux/SSLPing.git
Cloning into 'SSLPing'...
[... output snipped ...]

Так как моя версия Java раньше 1.8.0_101 (не выпущенная на момент написания этой статьи), сертификат Let Encrypt не будет проверяться по умолчанию. Посмотрите, как выглядит ошибка перед применением исправления:

$ java -jar SSLPing/dist/SSLPing.jar helloworld.letsencrypt.org 443
About to connect to 'helloworld.letsencrypt.org' on port 443
javax.net.ssl.SSLHandshakeException: sun.security.validator.ValidatorException: PKIX path building failed: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target
[... output snipped ...]

2. Импортировать сертификат

Я нахожусь в Mac OS X с набором переменных среды JAVA_HOME. Позже команды предполагают, что эта переменная установлена ​​для установки java, которую вы изменяете:

$ echo $JAVA_HOME 
/Library/Java/JavaVirtualMachines/jdk1.8.0_92.jdk/Contents/Home/

Сделайте резервную копию файла cacerts, который мы будем изменять, чтобы вы могли отменить любые изменения, не переустанавливая JDK:

$ sudo cp -a $JAVA_HOME/jre/lib/security/cacerts $JAVA_HOME/jre/lib/security/cacerts.orig

Загрузите сертификат подписи, который нам нужно импортировать:

$ wget https://letsencrypt.org/certs/lets-encrypt-x3-cross-signed.der

Выполните импорт:

$ sudo keytool -trustcacerts -keystore $JAVA_HOME/jre/lib/security/cacerts -storepass changeit -noprompt -importcert -alias lets-encrypt-x3-cross-signed -file lets-encrypt-x3-cross-signed.der 
Certificate was added to keystore

3. Убедитесь, что он работает после изменений

Убедитесь, что Java теперь счастлив подключиться к порту SSL:

$ java -jar SSLPing/dist/SSLPing.jar helloworld.letsencrypt.org 443
About to connect to 'helloworld.letsencrypt.org' on port 443
Successfully connected

Ответ 4

Для JDK, которые еще не поддерживают Let Encrypt certificate, вы можете добавить их в JDK cacerts после этого процесса (спасибо this).

Загрузите все сертификаты на https://letsencrypt.org/certificates/ (выберите формат der) и добавьте их по одному с такой командой (пример для letsencryptauthorityx1.der):

keytool -import -keystore PATH_TO_JDK\jre\lib\security\cacerts -storepass changeit -noprompt -trustcacerts -alias letsencryptauthorityx1 -file PATH_TO_DOWNLOADS\letsencryptauthorityx1.der