В Java, какой самый простой способ создать SSLContext только с файлом PEM?

Я использовал CertBot LetsEncrypt для генерации файлов PEM бесплатно. На других языках легко запустить HTTPS-сервер, используя всего пару строк кода и файлы PEM/key. Решения, которые я нашел в Java, слишком сложны, и я ищу что-то более простое.

  1. Я не хочу использовать командную строку Java "keytool". Я просто хочу перетащить мои файлы PEM/key в мое затмение и программно запустить HTTPS-сервер, используя SSLContext.
  2. Я не хочу включать массивные внешние библиотеки, такие как BouncyCastle. Смотрите следующую ссылку для предполагаемого решения с использованием BouncyCastle: Как создать SSLSocketFactory из сертификата PEM и ключа без преобразования в хранилище ключей?

Есть ли лучший / более простой способ сделать это?

2 ответа

Хотя ответ был дан, я хотел бы предложить альтернативу, которая также может сработать для вас. См. Ниже пример настройки:

X509ExtendedKeyManager keyManager = PemUtils.loadIdentityMaterial("certificate-chain.pem", "private-key.pem", "private-key-password".toCharArray());
X509ExtendedTrustManager trustManager = PemUtils.loadTrustMaterial("some-trusted-certificate.pem");

SSLFactory sslFactory = SSLFactory.builder()
          .withIdentityMaterial(keyManager)
          .withTrustMaterial(trustManager)
          .build();

SSLContext sslContext = sslFactory.getSslContext();

Чтобы использовать указанную выше настройку, вы можете использовать эту библиотеку:

<dependency>
    <groupId>io.github.hakky54</groupId>
    <artifactId>sslcontext-kickstart-for-pem</artifactId>
    <version>5.4.0</version>
</dependency>

Приведенная выше установка требует меньшего количества настраиваемого кода, пока она все еще выполняет то, что вы пытаетесь выполнить.

Следующий код в целом показывает, как создать SSLContext для HTTPS-сервера путем анализа файла PEM с несколькими записями, например, несколькими сертификатами и одним RSA PRIVATE KEY, Однако он неполон, потому что простая Java 8 не может проанализировать данные закрытого ключа RSA PKCS#1. Поэтому кажется, что ваше желание сделать это без какой-либо библиотеки невозможно. По крайней мере, BouncyCastle для анализа данных PKCS#1 требуется (и тогда можно использовать PEM-анализатор BouncyCastle).

private SSLContext createSslContext() throws Exception {
    URL url = getClass().getResource("/a.pem");
    InputStream in = url.openStream();
    String pem = new String(in.readAllBytes(), StandardCharsets.UTF_8);
    Pattern parse = Pattern.compile("(?m)(?s)^---*BEGIN ([^-]+)---*$([^-]+)^---*END[^-]+-+$");
    Matcher m = parse.matcher(pem);
    CertificateFactory certFactory = CertificateFactory.getInstance("X.509");
    Decoder decoder = Base64.getMimeDecoder();
    List<Certificate> certList = new ArrayList<>(); // java.security.cert.Certificate

    PrivateKey privateKey = null;

    int start = 0;
    while (m.find(start)) {
        String type = m.group(1);
        String base64Data = m.group(2);
        byte[] data = decoder.decode(base64Data);
        start += m.group(0).length();
        type = type.toUpperCase();
        if (type.contains("CERTIFICATE")) {
            Certificate cert = certFactory.generateCertificate(new ByteArrayInputStream(data));
            certList.add(cert);
        } else if (type.contains("RSA PRIVATE KEY")) {
            // TODO: load and parse PKCS1 data structure to get the RSA private key  
            privateKey = ...
        } else {
            System.err.println("Unsupported type: " + type);
        }

    }
    if (privateKey == null)
        throw new RuntimeException("RSA private key not found in PEM file");

    char[] keyStorePassword = new char[0];

    KeyStore keyStore = KeyStore.getInstance("JKS");
    keyStore.load(null, null);

    int count = 0;
    for (Certificate cert : certList) {
        keyStore.setCertificateEntry("cert" + count, cert);
        count++;
    }
    Certificate[] chain = certList.toArray(new Certificate[certList.size()]);
    keyStore.setKeyEntry("key", privateKey, keyStorePassword, chain);

    TrustManagerFactory tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
    tmf.init(keyStore);
    KeyManagerFactory kmf = KeyManagerFactory.getInstance("RSA");
    kmf.init(keyStore, keyStorePassword);
    SSLContext sslContext = SSLContext.getInstance("TLS");
    sslContext.init(kmf.getKeyManagers(), tmf.getTrustManagers(), new SecureRandom());
    return sslContext;
}

Мое полное решение, которое я сейчас использую:

  1. Используйте certbot на своем сервере для создания сертификата. Я использую команду "certbot certonly -d myawesomedomain.com"
  2. Я использую следующий код для преобразования этого сертификата certbot в java SSLContext: https://github.com/mirraj2/bowser/blob/master/src/bowser/SSLUtils.java
package bowser;

import static com.google.common.base.Preconditions.checkState;
import static ox.util.Utils.propagate;

import java.io.File;
import java.security.KeyStore;

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

import com.google.common.base.Splitter;

import ox.IO;
import ox.Log;

public class SSLUtils {

  public static SSLContext createContext(String domain) {
    String pass = "spamspam";

    File dir = new File("/etc/letsencrypt/live/" + domain);
    if (!dir.exists()) {
      Log.warn("Could not find letsencrypt dir: " + dir);
      return null;
    }

    File keystoreFile = new File(dir, "keystore.jks");
    File pemFile = new File(dir, "fullchain.pem");

    boolean generateKeystore = false;

    if (keystoreFile.exists()) {
      if (keystoreFile.lastModified() < pemFile.lastModified()) {
        Log.info("SSUtils: It looks like a new PEM file was created. Regenerating the keystore.");
        keystoreFile.delete();
        generateKeystore = true;
      }
    } else {
      generateKeystore = true;
    }

    if (generateKeystore) {
      Splitter splitter = Splitter.on(' ');
      try {
        String command = "openssl pkcs12 -export -out keystore.pkcs12 -in fullchain.pem -inkey privkey.pem -passout pass:"
            + pass;
        Log.debug(command);
        Process process = new ProcessBuilder(splitter.splitToList(command))
            .directory(dir).inheritIO().start();
        checkState(process.waitFor() == 0);

        command = "keytool -importkeystore -srckeystore keystore.pkcs12 -srcstoretype PKCS12 -destkeystore keystore.jks -srcstorepass "
            + pass + " -deststorepass " + pass;
        Log.debug(command);
        process = new ProcessBuilder(splitter.splitToList(command))
            .directory(dir).inheritIO().start();
        checkState(process.waitFor() == 0);

        new File(dir, "keystore.pkcs12").delete();// cleanup
      } catch (Exception e) {
        throw propagate(e);
      }
    }

    try {
      KeyStore keystore = KeyStore.getInstance(KeyStore.getDefaultType());
      keystore.load(IO.from(keystoreFile).asStream(), pass.toCharArray());

      KeyManagerFactory keyManagerFactory = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
      keyManagerFactory.init(keystore, pass.toCharArray());

      SSLContext ret = SSLContext.getInstance("TLSv1.2");
      TrustManagerFactory factory = TrustManagerFactory.getInstance(
          TrustManagerFactory.getDefaultAlgorithm());
      factory.init(keystore);
      ret.init(keyManagerFactory.getKeyManagers(), factory.getTrustManagers(), null);

      return ret;
    } catch (Exception e) {
      throw propagate(e);
    }
  }

}

Вы можете сделать что-то вроде этого, чтобы получить правильный SSLContext:

final String ca1 = "..load PEM file in string..";
final CertificateFactory cf = CertificateFactory.getInstance("X.509");
final Certificate cert1 = cf.generateCertificate(new ByteArrayInputStream(ca1.getBytes()));
final KeyStore keyStore = KeyStore.getInstance("JKS");
keyStore.load(null, null);
// Can add multiple truststore certificates here...
keyStore.setCertificateEntry("my-ca-1", cert1);
final SSLContext sslContext = SSLContexts.custom().setKeyStoreType(saToken)
        .loadTrustMaterial(keyStore, null)
        .build();
  • Это временное решение, потому что приведенный ниже код позволяет вам принять любой сервер, поэтому вам следует тщательно проверять свой код, когда вы пробуете решения такого рода.

  • Этот код не нуждается ни в каком сертификате вообще.

  • Вопрос в том, почему вы пытаетесь избежать этого процесса, если запрашиваемое дело не должно использовать небезопасный сервер?


logger.info("Starting instance ");
        TrustManager[] tm = new TrustManager[]{new X509TrustManager() {
            public X509Certificate[] getAcceptedIssuers(){return new X509Certificate[]{};}
            public void checkClientTrusted(X509Certificate[] chain, String authType) {logger.info(" checkClientTrusted");}
            public void checkServerTrusted(X509Certificate[] chain, String authType) {logger.info(" checkServerTrusted");}

        }};

        SSLContext sslContext = SSLContext.getInstance("SSL");
        sslContext.init(null, tm , new SecureRandom());
        HttpsURLConnection.setDefaultSSLSocketFactory(sslContext.getSocketFactory());
Другие вопросы по тегам