25

I used LetsEncrypt's CertBot to generate PEM files for free. In other languages it is easy to start an HTTPS server using just a couple lines of code and the PEM/key files. The solutions I have found so far in java are overly complex and I'm looking for something simpler.

  1. I do not want to use java's command-line "keytool". I just want to drag and drop my PEM/key files into my eclipse, and programatically start up an HTTPS server using an SSLContext.
  2. I do not want to include massive external libraries like BouncyCastle. See the following link for a supposed solution using BouncyCastle: How to build a SSLSocketFactory from PEM certificate and key without converting to keystore?

Is there a better/easier way to do this?

satnam
  • 10,719
  • 5
  • 32
  • 42
  • 3
    CertBot (which is actually from EFF not LetsEncrypt) normally produces several PEM files and you need all of them not just one. Tomcat 8.5 and 9 can (as a new feature) be configured directly with PEM files even when not using APR, but I don't know how or if they integrate with Eclipse. – dave_thompson_085 May 01 '18 at 16:50
  • If you have the key in `-----BEGIN RSA PRIVATE KEY-----` format loading it is pretty complicated as it is PKCS#1 format which AFAIK can not be read easily in plain Java. – Robert May 02 '18 at 18:03
  • In Java, you don't commonly programatically create servers. This is why the process is not as straight forward as you may expect. Instead, you create an app using the common web primitives, and _deploy_ it on a standard server, like Tomcat, which you configure for HTTPs. There are tools and frameworks that do not work this way, but start a server on their own and that's no longer the _plain_ Java way you seem to be after. – kaqqao May 05 '18 at 23:31
  • Assuming that a web app is in question, any reason why you can't have a web server like nginx or apache in front of your java server? They know how to handle pem files pretty well. – vl4d1m1r4 May 08 '18 at 12:52
  • 1
    I think that you have excluded all the easy solutions, sorry. There are easy solutions that involve converting the key to Java format, which you excluded with (1), or using a library that can read PEM, which you have excluded with (2). – Rich May 08 '18 at 12:53

5 Answers5

5

The following code shows in general how create a SSLContext for an HTTPS server by parsing a PEM file that has multiple entries, e.g. several certificates and one RSA PRIVATE KEY. However it is incomplete because plain Java 8 is unable to parse the PKCS#1 RSA private key data. Therefore it seems that your wish to do it without any library is not possible. At least BouncyCastle for parsing the PKCS#1 data is required (and then the PEM parser of BouncyCastle could be used, too).

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;
}
Robert
  • 39,162
  • 17
  • 99
  • 152
  • 2
    `CertificateFactory` can already read cert in PEM format (as well as DER). However a trustedCertEntry containing the cert is **useless** for an SSL/TLS server; the server needs the cert AND chain AND privatekey in a privateKeyEntry. The second Q you link covers this, but works in plain Java only if the privatekey file is PKCS8 and unencrypted, not PKCS1/traditional or encrypted which require BC. Are you saying certbot always produces PKCS8 unencrypted, or at least in this Q's unspecified case? – dave_thompson_085 May 01 '18 at 16:37
  • @dave_thompson_085 You are right, the answer is just for a client context, not for a server. In the question there was only mentioned the PEM file, but nothing about a key file. However as Let's Encrypt is mentioned it looks like it is about an SSL server context. – Robert May 02 '18 at 08:27
  • @Robert, I updated the question. To clarify, this is for the server. My intention was to just drag and drop in the file(s) and start the server. I guess this may have to be multiple PEM files and the key file, although LetsEncrypt includes a PEM that *I think* contains all of the needed data in a single PEM, so would just need that and the key. – satnam May 02 '18 at 14:56
  • @satnam You should exactly specify what PEM files and key file(s) are present. Otherwise it is very difficult to create a working answer. – Robert May 02 '18 at 15:05
  • 1
    If you really do want to read PKCS1 _unencrypted_ PEM without BC, see https://stackoverflow.com/questions/23709898/java-convert-dkim-private-key-from-rsa-to-der-for-javamail and instead of writing the result put it in `PKCS8EncodedKeySpec` and run it through `KeyFactory.getInstance("RSA")` – dave_thompson_085 May 04 '18 at 07:51
3

Although an answer has been provided, I would like to provide an alternative which requires less code. See below for an example setup:

import nl.altindag.ssl.SSLFactory;
import nl.altindag.ssl.util.PemUtils;

import javax.net.ssl.SSLContext;
import javax.net.ssl.X509ExtendedKeyManager;
import javax.net.ssl.X509ExtendedTrustManager;

public class App {

    public static void main(String[] args) {
        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();
    }
    
}

To use the above setup you can use this library:

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

The setup above requires less custom code while it is still achieving what you are trying to accomplish. You can view the library and documentation here: https://github.com/Hakky54/sslcontext-kickstart

Hakan54
  • 3,121
  • 1
  • 23
  • 37
  • I'd very much like to try this. Sadly, my IDE (VSCode) complains about the hyphen in the `artifactId`. I notice in your github that you've apparently changed the name (to `sslcontext-kickstart`), but maven still refuses to import it. I'm not expert in any of these Java tools, so please be gentle. – Tom Stambaugh Apr 08 '23 at 16:23
  • The complaint in the IDE is `Syntax error on token "-", . expectedJava(1610612940)`. I also see a red squiggle at the front of the import line that says `The import io cannot be resolvedJava(268435846)` – Tom Stambaugh Apr 08 '23 at 16:26
  • @TomStambaugh Can you publish your repo to GitHub so I can try it out on my side? – Hakan54 Apr 08 '23 at 16:40
  • 1
    I figured out the issue. The package name used in the code is different from the groupId in the dependency. The import statement that works is `import nl.altindag.ssl.util.PemUtils;`. My server is now working great! – Tom Stambaugh Apr 09 '23 at 17:10
1

My full solution that I currently use:

  1. Use certbot on your server to generate the certificate. I use the command "certbot certonly -d myawesomedomain.com"
  2. I use the following code to convert that certbot certificate into a 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);
    }
  }

}

satnam
  • 10,719
  • 5
  • 32
  • 42
  • 1
    You can use PKCS12 directly, without converting to JKS, in any Java since about 2005, and since Java 9 in 2017 PKCS12 is _preferred_ and JKS deprecated (though not, yet, actually dropped). Also SSLv3 _protocol_ is obsolete and no longer permitted at all; in practice when you ask for it in a context JSSE will give you (different) usable protocol(s) but not necessarily the best, so better to ask for something good or at least `"Default"`. – dave_thompson_085 Mar 19 '20 at 19:42
  • thanks, this code was from a while ago, I'm going to update it now based on your advice – satnam Mar 19 '20 at 19:49
1

You can do something like this to get a proper 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();
Vedran Vidovic
  • 1,133
  • 6
  • 6
-4
  • This is a temporarly solution, because the code below, alows you to accept any server so, you should check deeply into your code when you are trying these kinds solutions.

  • This code does not need any cert at all.

  • The question is that why you are trying to avoid this procces, if the case requiered shouldn´t you be using a non secure server ?


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());
  • 1
    It looks like that code is for connecting to a server. I'm trying to start a server. – satnam May 02 '18 at 14:57
  • actually the code is creating and setting up a context, so the last line is of couse when you want to conect. Insted of it, you can do this: ------------------------------------------------------------------------------------------------- // Create socket factory SSLSocketFactory sslSocketFactory = sslContext.getSocketFactory();------------------------------------------------------------------------------------------------------------------------------------------------------- SSLSocket sslSocket = (SSLSocket) sslSocketFactory.createSocket(host, port); – David Arellano May 02 '18 at 15:48
  • // Create socket factory SSLSocketFactory sslSocketFactory = sslContext.getSocketFactory(); // Create socket SSLSocket sslSocket = (SSLSocket) sslSocketFactory.createSocket(host,port); – David Arellano May 02 '18 at 15:56
  • This is client code. It is all about trusting other people's certificates, rather than using your own. It doesn't solve the question that was asked in any way shape or form. – user207421 Mar 19 '20 at 23:45