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.
Is there a better/easier way to do this?
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;
}
Although an answer has been provided, I would like to provide an alternative which requires less code. See below for an example setup:
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>7.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
My full solution that I currently use:
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);
}
}
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With