3

I opted to use acme4j to create a letsencrypt certificate. So far it seems to have worked perfectly and I have some java code that creates a registration, responds to a challenge an ultimately presents me with a x509 certificate for my domain (along with a 'certificate chain'). The code is integrated nicely into my java application and doesn't require any downtime for certificate renewal. Awesome.

From here I'm a bit stuck. My application is a just a main app that has an embedded undertow webserver that I instantiate programatically. In order to create an https listener I need to create an SSLContext object. I've saved the x509 certificate that I got from letsencrypt to disk so it can be reused:

    ...
    X509Certificate x509 = cert.download();

    Path pemFile = pathTo(domain + ".pem");

    try (Writer writer = Files.newBufferedWriter(pemFile); JcaPEMWriter jcaPEMWriter = new JcaPEMWriter(writer)) {
        jcaPEMWriter.writeObject(x509);
    }

And then on start up my application reloads that certificate and passes it into the undertow web server:

    CertificateFactory cf = CertificateFactory.getInstance("X.509");
    FileInputStream finStream = new FileInputStream(certFile.toFile());
    X509Certificate x509Certificate = (X509Certificate)cf.generateCertificate(finStream);

    KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType());
    keyStore.load(null);
    keyStore.setCertificateEntry("someAlias", x509Certificate);

    TrustManagerFactory instance = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
    instance.init(keyStore);

    SSLContext sslContext = SSLContext.getInstance("TLS");
    sslContext.init(null, instance.getTrustManagers(), null);
    SSLContext.setDefault(sslContext);

    Undertow.Builder builder = Undertow.builder();
    builder.addHttpsListener(httpsPort, ipAddress, sslContext);

The app starts, can't see any errors or warnings until I try and hit an https endpoint where Chrome just shows _ERR_CONNECTION_CLOSED_. I turned on -Djavax.net.debug=all to try and see whats going on:

%% Initialized:  [Session-12, SSL_NULL_WITH_NULL_NULL]
XNIO-1 task-12, fatal error: 40: no cipher suites in common
javax.net.ssl.SSLHandshakeException: no cipher suites in common
%% Invalidated:  [Session-12, SSL_NULL_WITH_NULL_NULL]
XNIO-1 task-12, SEND TLSv1.1 ALERT:  fatal, description = handshake_failure
XNIO-1 task-12, WRITE: TLSv1.1 Alert, length = 2
XNIO-1 I/O-2, fatal: engine already closed.  Rethrowing javax.net.ssl.SSLHandshakeException: no cipher suites in common
XNIO-1 I/O-2, called closeInbound()
XNIO-1 I/O-2, fatal: engine already closed.  Rethrowing javax.net.ssl.SSLException: Inbound closed before receiving peer's close_notify: possible truncation attack?
2016-09-08 08:34:46,861 DEBUG [io] - UT005013: An IOException occurred
java.io.IOException: javax.net.ssl.SSLException: Inbound closed before receiving peer's close_notify: possible truncation attack?

I'm trying to come up with a pure java solution here. Something that is repeatable, in code and can be tested and checked in to source control. I want to avoid having to do any out-of-jvm machine level set up if possible.

After lots of hackery and reading, it seems like I need to use the keytool and some combination of the certificate I was issued, along with the certificate chain I was issued, along with the root certificate and some/none/all of the intermediate letsencrypt certificates! Seriously?

I tried following the instructions here from the section titled "7.3.1.3. Using an existing Certificate" but only to end up with exactly the same error.

Any help would be greatly appreciated.

pomo
  • 2,251
  • 1
  • 21
  • 34

1 Answers1

3

An SSL/TLS server (in any language) requires a PRIVATE KEY AND certificate (and nearly always a cert chain) not just a certificate. And a Java SSL/TLS server needs that privatekey+certchain in a KeyManager not a TrustManager -- any advice that recommended you set only a TrustManager for an SSL/TLS server is totally incompetent. The section 7.3.1.3 you link to makes no sense, although the next section 7.3.1.4, although it claims a bug I have never seen and I have used just about every version of OpenSSL, does describe almost the right way to convert OpenSSL key+cert to Java, which matches the earlier sections which described creating keys and certs in OpenSSL format. But you don't have OpenSSL format.

That acme4j page correctly says you should "generate a separate pair of keys" -- pair meaning private and public -- which the earlier "How to Use" page told you how to do with their KeyPairUtils, then generate a CSR and send it in, and get the certificate and chain. It specifies a single call

X509Certificate[] chain = cert.downloadChain()

which might be what you need if LetsEncrypt is clever; look at chain[0].getSubjectX500Principal() and see if it's you. If not you probably need to do both download() and downloadChain() and put them together in one array with your cert first:

X509Certificate[] fixchain = new X509Certificate [chain.length+1];
fixchain[0] = mycert; System.arraycopy (chain,0, fixchain,1, chain.length);
chain = fixchain; // for simplicity

Once you have the cert chain AND the key pair, do something like

char[] password = /* some value, should be secure if used for file, 
    if used only in memory doesn't really matter */
KeyStore ks = KeyStore.getInstance("jks"/*or default if you don't care*/); 
ks.load (null); // above line and this same as you have now
ks.setKeyEntry ("alias", keypair.getPrivateKey(), password, chain);
// this line different -- KeyEntry not CertificateEntry

// if you want to save in a file for reuse
try( OutputStream os = new FileOutputStream ("blah") ){ ks.store (os, password); }
// if you want to save somewhere else, extrapolate 

// if/when you want to run server
KeyManagerFactory kmf = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
kmf.init(ks, password); // same password as above (or when stored)
// MAYBE TrustManager if you want to require certs FROM CLIENTS
// in which case the certs you trust probably shouldn't be limited to LetsEncrypt
SSLContext ctx = SSLContext.getInstance("TLS";
ctx.init(kmf.getKeyManagers(), null /*or tmf.getTrustManagers()*/, null);
SSLContext.setDefault (ctx); // or otherwise use ctx

FYI if a certificate had actually been what you needed to store and read, you don't need to convert it to PEM unless you want people to cut&paste etc. Java CertificateFactory has handled both DER and PEM for at least a decade.

dave_thompson_085
  • 34,712
  • 6
  • 50
  • 70