diff --git a/acme4j-client/src/main/java/org/shredzone/acme4j/provider/pebble/PebbleAcmeProvider.java b/acme4j-client/src/main/java/org/shredzone/acme4j/provider/pebble/PebbleAcmeProvider.java index 634764cd..0f884b22 100644 --- a/acme4j-client/src/main/java/org/shredzone/acme4j/provider/pebble/PebbleAcmeProvider.java +++ b/acme4j-client/src/main/java/org/shredzone/acme4j/provider/pebble/PebbleAcmeProvider.java @@ -19,6 +19,7 @@ import java.net.URL; import java.util.regex.Matcher; import java.util.regex.Pattern; +import org.shredzone.acme4j.connector.HttpConnector; import org.shredzone.acme4j.provider.AbstractAcmeProvider; import org.shredzone.acme4j.provider.AcmeProvider; @@ -47,7 +48,7 @@ public class PebbleAcmeProvider extends AbstractAcmeProvider { try { String path = serverUri.getPath(); - URL baseUrl = new URL("http://localhost:14000/dir"); + URL baseUrl = new URL("https://localhost:14000/dir"); if (path != null && !path.isEmpty() && !"/".equals(path)) { baseUrl = parsePath(path); @@ -74,10 +75,15 @@ public class PebbleAcmeProvider extends AbstractAcmeProvider { if (m.group(2) != null) { port = Integer.parseInt(m.group(2)); } - return new URL("http", host, port, "/dir"); + return new URL("https", host, port, "/dir"); } else { throw new IllegalArgumentException("Invalid Pebble host/port: " + path); } } + @Override + protected HttpConnector createHttpConnector() { + return new PebbleHttpConnector(); + } + } diff --git a/acme4j-client/src/main/java/org/shredzone/acme4j/provider/pebble/PebbleHttpConnector.java b/acme4j-client/src/main/java/org/shredzone/acme4j/provider/pebble/PebbleHttpConnector.java new file mode 100644 index 00000000..320a097f --- /dev/null +++ b/acme4j-client/src/main/java/org/shredzone/acme4j/provider/pebble/PebbleHttpConnector.java @@ -0,0 +1,75 @@ +/* + * acme4j - Java ACME client + * + * Copyright (C) 2017 Richard "Shred" Körber + * http://acme4j.shredzone.org + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + */ +package org.shredzone.acme4j.provider.pebble; + +import java.io.IOException; +import java.net.HttpURLConnection; +import java.net.URL; +import java.security.KeyManagementException; +import java.security.KeyStore; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; +import java.security.cert.CertificateException; + +import javax.net.ssl.HttpsURLConnection; +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLSocketFactory; +import javax.net.ssl.TrustManagerFactory; + +import org.shredzone.acme4j.connector.HttpConnector; + +/** + * {@link HttpConnector} to be used for Pebble. Pebble uses a static, self signed SSL + * certificate. + */ +public class PebbleHttpConnector extends HttpConnector { + + private static SSLSocketFactory sslSocketFactory; + + @Override + public HttpURLConnection openConnection(URL url) throws IOException { + HttpURLConnection conn = super.openConnection(url); + if (conn instanceof HttpsURLConnection) { + ((HttpsURLConnection) conn).setSSLSocketFactory(createSocketFactory()); + } + return conn; + } + + /** + * Lazily creates an {@link SSLSocketFactory} that exclusively accepts the Pebble + * certificate. + */ + protected synchronized SSLSocketFactory createSocketFactory() throws IOException { + if (sslSocketFactory == null) { + try { + KeyStore keystore = KeyStore.getInstance(KeyStore.getDefaultType()); + keystore.load(getClass().getResourceAsStream("/org/shredzone/acme4j/provider/pebble/pebble.truststore"), + "acme4j".toCharArray()); + + TrustManagerFactory tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); + tmf.init(keystore); + + SSLContext ctx = SSLContext.getInstance("TLS"); + ctx.init(null, tmf.getTrustManagers(), null); + + sslSocketFactory = ctx.getSocketFactory(); + } catch (KeyStoreException | CertificateException | NoSuchAlgorithmException + | KeyManagementException ex) { + throw new IOException("Could not create truststore", ex); + } + } + return sslSocketFactory; + } + +} diff --git a/acme4j-client/src/main/resources/org/shredzone/acme4j/provider/pebble/pebble.truststore b/acme4j-client/src/main/resources/org/shredzone/acme4j/provider/pebble/pebble.truststore new file mode 100644 index 00000000..749bc26e Binary files /dev/null and b/acme4j-client/src/main/resources/org/shredzone/acme4j/provider/pebble/pebble.truststore differ diff --git a/acme4j-client/src/test/java/org/shredzone/acme4j/provider/pebble/PebbleAcmeProviderTest.java b/acme4j-client/src/test/java/org/shredzone/acme4j/provider/pebble/PebbleAcmeProviderTest.java index 1298bdf4..ac30ca70 100644 --- a/acme4j-client/src/test/java/org/shredzone/acme4j/provider/pebble/PebbleAcmeProviderTest.java +++ b/acme4j-client/src/test/java/org/shredzone/acme4j/provider/pebble/PebbleAcmeProviderTest.java @@ -51,15 +51,15 @@ public class PebbleAcmeProviderTest { PebbleAcmeProvider provider = new PebbleAcmeProvider(); assertThat(provider.resolve(new URI("acme://pebble")), - is(url("http://localhost:14000/dir"))); + is(url("https://localhost:14000/dir"))); assertThat(provider.resolve(new URI("acme://pebble/")), - is(url("http://localhost:14000/dir"))); + is(url("https://localhost:14000/dir"))); assertThat(provider.resolve(new URI("acme://pebble/pebble.example.com")), - is(url("http://pebble.example.com:14000/dir"))); + is(url("https://pebble.example.com:14000/dir"))); assertThat(provider.resolve(new URI("acme://pebble/pebble.example.com:12345")), - is(url("http://pebble.example.com:12345/dir"))); + is(url("https://pebble.example.com:12345/dir"))); assertThat(provider.resolve(new URI("acme://pebble/pebble.example.com:12345/")), - is(url("http://pebble.example.com:12345/dir"))); + is(url("https://pebble.example.com:12345/dir"))); try { provider.resolve(new URI("acme://pebble/bad.example.com:port")); diff --git a/acme4j-it/src/test/java/org/shredzone/acme4j/it/PebbleITBase.java b/acme4j-it/src/test/java/org/shredzone/acme4j/it/PebbleITBase.java index 13a6074b..fb615b76 100644 --- a/acme4j-it/src/test/java/org/shredzone/acme4j/it/PebbleITBase.java +++ b/acme4j-it/src/test/java/org/shredzone/acme4j/it/PebbleITBase.java @@ -76,7 +76,7 @@ public abstract class PebbleITBase { */ protected void assertIsPebbleUrl(URL url) { assertThat(url, not(nullValue())); - assertThat(url.getProtocol(), is("http")); + assertThat(url.getProtocol(), is("https")); assertThat(url.getHost(), is(pebbleHost)); assertThat(url.getPort(), is(pebblePort)); assertThat(url.getPath(), not(isEmptyOrNullString())); diff --git a/acme4j-it/src/test/pebble/pebble-config.json b/acme4j-it/src/test/pebble/pebble-config.json index 6f51b3d4..a2d7f7b6 100644 --- a/acme4j-it/src/test/pebble/pebble-config.json +++ b/acme4j-it/src/test/pebble/pebble-config.json @@ -1,6 +1,8 @@ { "pebble": { "listenAddress": "0.0.0.0:14000", + "certificate": "/go/src/github.com/letsencrypt/pebble/test/certs/localhost/cert.pem", + "privateKey": "/go/src/github.com/letsencrypt/pebble/test/certs/localhost/key.pem", "httpPort": 5002, "tlsPort": 5001 }