【问题标题】:In Java, what is the simplest way to create an SSLContext with just a PEM file?在 Java 中,仅使用 PEM 文件创建 SSLContext 的最简单方法是什么?
【发布时间】:2018-10-06 02:19:06
【问题描述】:

我使用 LetsEncrypt 的 CertBot 免费生成 PEM 文件。在其他语言中,只需几行代码和 PEM/密钥文件即可轻松启动 HTTPS 服务器。到目前为止,我在 java 中找到的解决方案过于复杂,我正在寻找更简单的解决方案。

  1. 我不想使用 java 的命令行“keytool”。我只想将我的 PEM/密钥文件拖放到我的 Eclipse 中,并使用 SSLContext 以编程方式启动 HTTPS 服务器。
  2. 我不想包含像 BouncyCastle 这样的大量外部库。有关使用 BouncyCastle 的假定解决方案,请参阅以下链接:How to build a SSLSocketFactory from PEM certificate and key without converting to keystore?

有没有更好/更简单的方法来做到这一点?

【问题讨论】:

  • CertBot(实际上来自 EFF 而不是 LetsEncrypt)通常会生成多个 PEM 文件,您需要所有这些文件而不仅仅是一个。即使不使用 APR,Tomcat 8.5 和 9 也可以(作为一项新功能)直接使用 PEM 文件进行配置,但我不知道它们如何或是否与 Eclipse 集成。
  • 如果你有 -----BEGIN RSA PRIVATE KEY----- 格式的密钥,加载它是相当复杂的,因为它是 PKCS#1 格式,AFAIK 不能用普通的 Java 轻松读取。
  • 在 Java 中,您通常不会以编程方式创建服务器。这就是为什么这个过程不像你想象的那么简单。相反,您可以使用通用 Web 原语创建应用程序,然后将其部署到标准服务器上,例如为 HTTPs 配置的 Tomcat。有些工具和框架不能以这种方式工作,而是自行启动服务器,而这不再是您所追求的 plain Java 方式。
  • 假设有一个 Web 应用程序存在问题,为什么您的 Java 服务器前面不能有一个像 nginx 或 apache 这样的 Web 服务器?他们知道如何很好地处理 pem 文件。
  • 我认为您已经排除了所有简单的解决方案,抱歉。有一些简单的解决方案涉及将密钥转换为 Java 格式,您已使用 (1) 排除,或者使用可以读取 PEM 的库,您已使用 (2) 排除。

标签: java ssl https java-8 lets-encrypt


【解决方案1】:

以下代码一般显示了如何通过解析具有多个条目的 PEM 文件来为 HTTPS 服务器创建 SSLContext,例如几个证书和一个RSA PRIVATE KEY。但是它是不完整的,因为普通的 Java 8 无法解析 PKCS#1 RSA 私钥数据。因此,您似乎无法在没有任何库的情况下做到这一点。至少需要 BouncyCastle 来解析 PKCS#1 数据(然后也可以使用 BouncyCastle 的 PEM 解析器)。

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;
}

【讨论】:

  • CertificateFactory 已经可以读取 PEM 格式的证书(以及 DER)。但是,对于 SSL/TLS 服务器,包含证书的trustedCertEntry 无用;服务器需要 privateKeyEntry 中的证书和链和私钥。您链接的第二个 Q 涵盖了这一点,但仅当私钥文件是 PKCS8 且未加密,而不是需要 BC 的 PKCS1/传统或加密时,才能在纯 Java 中使用。你是说 certbot 总是产生未加密的 PKCS8,或者至少在这个 Q 的未指定情况下?
  • @dave_thompson_085 你是对的,答案只是针对客户端上下文,而不是针对服务器。在问题中只提到了 PEM 文件,但没有提到密钥文件。然而,正如 Let's Encrypt 提到的,它看起来像是关于 SSL 服务器上下文。
  • @Robert,我更新了这个问题。澄清一下,这是针对服务器的。我的意图是拖放文件并启动服务器。我想这可能必须是多个 PEM 文件和密钥文件,尽管 LetsEncrypt 包含一个 PEM,我认为在单个 PEM 中包含所有需要的数据,所以只需要那个和密钥。
  • @satnam 您应该准确指定存在哪些 PEM 文件和密钥文件。否则很难创建一个有效的答案。
  • 如果您确实想在没有 BC 的情况下读取 PKCS1 未加密 PEM,请参阅stackoverflow.com/questions/23709898/…,而不是将结果写入PKCS8EncodedKeySpec 并通过@987654326 运行@
【解决方案2】:
  • 这是一个临时解决方案,因为下面的代码允许您接受任何服务器,因此,当您尝试这些解决方案时,您应该深入检查您的代码。

  • 此代码根本不需要任何证书。

  • 问题是为什么你要避免这个过程,如果需要的话,你不应该使用不安全的服务器吗?


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());

【讨论】:

  • 看起来该代码用于连接服务器。我正在尝试启动服务器。
  • 实际上代码是在创建和设置上下文,所以当你想连接时,最后一行是很重要的。安装它,你可以这样做: ----------------------------------------- -------------------------------------------------- ------ // 创建套接字工厂 SSLSocketFactory sslSocketFactory = sslContext.getSocketFactory();----------------------------- -------------------------------------------------- -------------------------------------------------- --------------------- SSLSocket sslSocket = (SSLSocket) sslSocketFactory.createSocket(host, port);
  • // 创建套接字工厂 SSLSocketFactory sslSocketFactory = sslContext.getSocketFactory(); // 创建套接字 SSLSocket sslSocket = (SSLSocket) sslSocketFactory.createSocket(host,port);
  • 这是客户端代码。这完全是关于信任其他人的证书,而不是使用您自己的证书。它不能解决以任何形式或形式提出的问题。
【解决方案3】:

我目前使用的完整解决方案:

  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);
    }
  }

}

【讨论】:

  • 您可以直接使用 PKCS12,而无需转换为 JKS,在大约 2005 年以后的任何 Java 中,并且由于 2017 年的 Java 9 PKCS12 是首选并且不推荐使用 JKS(虽然还没有,但,实际上下降了)。 SSLv3 protocol 也已过时,根本不再允许使用;实际上,当您在上下文中提出要求时,JSSE 会为您提供(不同的)可用协议,但不一定是最好的,因此最好要求一些好的或至少 "Default"
  • 谢谢,这段代码是很久以前的,我现在根据你的建议更新它
【解决方案4】:

您可以执行以下操作来获得正确的 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();

【讨论】:

【解决方案5】:

虽然已经提供了答案,但我想提供一个需要更少代码的替代方案。请参阅下面的示例设置:

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>7.0.0</version>
</dependency>

上面的设置需要更少的自定义代码,而它仍然可以实现您想要实现的目标。您可以在此处查看库和文档:https://github.com/Hakky54/sslcontext-kickstart

【讨论】:

    猜你喜欢
    • 2014-01-28
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2023-03-17
    • 2019-06-25
    • 2014-05-16
    • 2017-04-05
    相关资源
    最近更新 更多