mirror of https://github.com/shred/acme4j
Add tls-alpn test server and integration test for Pebble
parent
677fce8f5f
commit
a089a307ff
|
@ -29,6 +29,11 @@
|
||||||
<description>acme4j Integration Tests</description>
|
<description>acme4j Integration Tests</description>
|
||||||
|
|
||||||
<properties>
|
<properties>
|
||||||
|
<!-- alpn-boot version must match the java version used in bammbamm -->
|
||||||
|
<!-- see: https://www.eclipse.org/jetty/documentation/9.4.x/alpn-chapter.html#alpn-versions -->
|
||||||
|
<alpn-java.version>8u162</alpn-java.version>
|
||||||
|
<alpn-boot.version>8.1.12.v20180117</alpn-boot.version>
|
||||||
|
|
||||||
<skipITs>true</skipITs>
|
<skipITs>true</skipITs>
|
||||||
<sonar.coverage.exclusions>src/main/java/org/shredzone/acme4j/**</sonar.coverage.exclusions>
|
<sonar.coverage.exclusions>src/main/java/org/shredzone/acme4j/**</sonar.coverage.exclusions>
|
||||||
</properties>
|
</properties>
|
||||||
|
@ -175,15 +180,16 @@
|
||||||
<name>acme4j/bammbamm:${project.version}</name>
|
<name>acme4j/bammbamm:${project.version}</name>
|
||||||
|
|
||||||
<build>
|
<build>
|
||||||
<from>openjdk:8-jre</from>
|
<from>openjdk:${alpn-java.version}-jre</from>
|
||||||
<ports>
|
<ports>
|
||||||
<port>53/udp</port>
|
<port>53/udp</port>
|
||||||
|
<port>5001</port>
|
||||||
<port>5002</port>
|
<port>5002</port>
|
||||||
<port>14001</port>
|
<port>14001</port>
|
||||||
</ports>
|
</ports>
|
||||||
<cmd>
|
<cmd>
|
||||||
<shell>
|
<shell>
|
||||||
java -cp "/maven/*" org.shredzone.acme4j.it.BammBamm
|
java -Xbootclasspath/p:/maven/alpn-boot-${alpn-boot.version}.jar -cp "/maven/*" org.shredzone.acme4j.it.BammBamm
|
||||||
</shell>
|
</shell>
|
||||||
</cmd>
|
</cmd>
|
||||||
<assembly>
|
<assembly>
|
||||||
|
@ -220,6 +226,11 @@
|
||||||
<version>${project.version}</version>
|
<version>${project.version}</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.mortbay.jetty.alpn</groupId>
|
||||||
|
<artifactId>alpn-boot</artifactId>
|
||||||
|
<version>${alpn-boot.version}</version>
|
||||||
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.nanohttpd</groupId>
|
<groupId>org.nanohttpd</groupId>
|
||||||
<artifactId>nanohttpd</artifactId>
|
<artifactId>nanohttpd</artifactId>
|
||||||
|
|
|
@ -21,6 +21,7 @@ import javax.annotation.ParametersAreNonnullByDefault;
|
||||||
|
|
||||||
import org.shredzone.acme4j.it.server.DnsServer;
|
import org.shredzone.acme4j.it.server.DnsServer;
|
||||||
import org.shredzone.acme4j.it.server.HttpServer;
|
import org.shredzone.acme4j.it.server.HttpServer;
|
||||||
|
import org.shredzone.acme4j.it.server.TlsAlpnServer;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
@ -28,8 +29,8 @@ import fi.iki.elonen.NanoHTTPD;
|
||||||
import fi.iki.elonen.router.RouterNanoHTTPD;
|
import fi.iki.elonen.router.RouterNanoHTTPD;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A mock server to test Pebble. It provides a HTTP server and DNS server. The servers can
|
* A mock server to test Pebble. It provides a HTTP server, TLS-ALPN server and DNS
|
||||||
* be configured remotely via simple HTTP POST requests.
|
* server. The servers can be configured remotely via simple HTTP POST requests.
|
||||||
* <p>
|
* <p>
|
||||||
* <em>WARNING:</em> This is a very simple server that is only meant to be used for
|
* <em>WARNING:</em> This is a very simple server that is only meant to be used for
|
||||||
* integration tests. Do not use in the outside world!
|
* integration tests. Do not use in the outside world!
|
||||||
|
@ -43,18 +44,22 @@ public class BammBamm {
|
||||||
private final int appPort;
|
private final int appPort;
|
||||||
private final int httpPort;
|
private final int httpPort;
|
||||||
private final int dnsPort;
|
private final int dnsPort;
|
||||||
|
private final int tlsAlpnPort;
|
||||||
private final AppServer appServer;
|
private final AppServer appServer;
|
||||||
private final DnsServer dnsServer;
|
private final DnsServer dnsServer;
|
||||||
private final HttpServer httpServer;
|
private final HttpServer httpServer;
|
||||||
|
private final TlsAlpnServer tlsAlpnServer;
|
||||||
|
|
||||||
private BammBamm() {
|
private BammBamm() {
|
||||||
ResourceBundle bundle = ResourceBundle.getBundle("bammbamm");
|
ResourceBundle bundle = ResourceBundle.getBundle("bammbamm");
|
||||||
appPort = Integer.parseInt(bundle.getString("app.port"));
|
appPort = Integer.parseInt(bundle.getString("app.port"));
|
||||||
dnsPort = Integer.parseInt(bundle.getString("dns.port"));
|
dnsPort = Integer.parseInt(bundle.getString("dns.port"));
|
||||||
httpPort = Integer.parseInt(bundle.getString("http.port"));
|
httpPort = Integer.parseInt(bundle.getString("http.port"));
|
||||||
|
tlsAlpnPort = Integer.parseInt(bundle.getString("tlsAlpn.port"));
|
||||||
|
|
||||||
dnsServer = new DnsServer();
|
dnsServer = new DnsServer();
|
||||||
httpServer = new HttpServer();
|
httpServer = new HttpServer();
|
||||||
|
tlsAlpnServer = new TlsAlpnServer();
|
||||||
appServer = new AppServer(appPort);
|
appServer = new AppServer(appPort);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -81,12 +86,20 @@ public class BammBamm {
|
||||||
return httpServer;
|
return httpServer;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the {@link TlsAlpnServer} instance.
|
||||||
|
*/
|
||||||
|
public TlsAlpnServer getTlsAlpnServer() {
|
||||||
|
return tlsAlpnServer;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Starts the servers.
|
* Starts the servers.
|
||||||
*/
|
*/
|
||||||
public void start() {
|
public void start() {
|
||||||
dnsServer.start(dnsPort);
|
dnsServer.start(dnsPort);
|
||||||
httpServer.start(httpPort);
|
httpServer.start(httpPort);
|
||||||
|
tlsAlpnServer.start(tlsAlpnPort);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
appServer.start(NanoHTTPD.SOCKET_READ_TIMEOUT, true);
|
appServer.start(NanoHTTPD.SOCKET_READ_TIMEOUT, true);
|
||||||
|
@ -102,6 +115,7 @@ public class BammBamm {
|
||||||
*/
|
*/
|
||||||
public void stop() {
|
public void stop() {
|
||||||
appServer.stop();
|
appServer.stop();
|
||||||
|
tlsAlpnServer.stop();
|
||||||
httpServer.stop();
|
httpServer.stop();
|
||||||
dnsServer.stop();
|
dnsServer.stop();
|
||||||
|
|
||||||
|
@ -123,6 +137,9 @@ public class BammBamm {
|
||||||
|
|
||||||
addRoute(HttpHandler.ADD, HttpHandler.Add.class);
|
addRoute(HttpHandler.ADD, HttpHandler.Add.class);
|
||||||
addRoute(HttpHandler.REMOVE, HttpHandler.Remove.class);
|
addRoute(HttpHandler.REMOVE, HttpHandler.Remove.class);
|
||||||
|
|
||||||
|
addRoute(TlsAlpnHandler.ADD, TlsAlpnHandler.Add.class);
|
||||||
|
addRoute(TlsAlpnHandler.REMOVE, TlsAlpnHandler.Remove.class);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -17,7 +17,12 @@ import java.io.IOException;
|
||||||
import java.io.UnsupportedEncodingException;
|
import java.io.UnsupportedEncodingException;
|
||||||
import java.net.URLEncoder;
|
import java.net.URLEncoder;
|
||||||
import java.nio.charset.Charset;
|
import java.nio.charset.Charset;
|
||||||
|
import java.security.PrivateKey;
|
||||||
|
import java.security.cert.CertificateEncodingException;
|
||||||
|
import java.security.cert.X509Certificate;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
import java.util.Base64;
|
||||||
|
import java.util.Base64.Encoder;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
import javax.annotation.ParametersAreNonnullByDefault;
|
import javax.annotation.ParametersAreNonnullByDefault;
|
||||||
|
@ -134,6 +139,41 @@ public class BammBammClient {
|
||||||
.submit();
|
.submit();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds a certificate for TLS-ALPN tests.
|
||||||
|
*
|
||||||
|
* @param alias
|
||||||
|
* An alias to be used for removal, for example the domain name being
|
||||||
|
* validated.
|
||||||
|
* @param privateKey
|
||||||
|
* {@link PrivateKey} of the certificate
|
||||||
|
* @param cert
|
||||||
|
* {@link X509Certificate} containing the domain name to respond to
|
||||||
|
*/
|
||||||
|
public void tlsAlpnAddCertificate(String alias, PrivateKey privateKey, X509Certificate cert) throws IOException {
|
||||||
|
try {
|
||||||
|
createRequest(TlsAlpnHandler.ADD)
|
||||||
|
.arg(":alias", alias)
|
||||||
|
.param("privateKey", privateKey.getEncoded())
|
||||||
|
.param("cert", cert.getEncoded())
|
||||||
|
.submit();
|
||||||
|
} catch (CertificateEncodingException ex) {
|
||||||
|
throw new IOException(ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes a certificate.
|
||||||
|
*
|
||||||
|
* @param alias
|
||||||
|
* Certificate alias to remove
|
||||||
|
*/
|
||||||
|
public void tlsAlpnRemoveCertificate(String alias) throws IOException {
|
||||||
|
createRequest(TlsAlpnHandler.REMOVE)
|
||||||
|
.arg(":alias", alias)
|
||||||
|
.submit();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a new {@link Request} object.
|
* Creates a new {@link Request} object.
|
||||||
*
|
*
|
||||||
|
@ -151,6 +191,7 @@ public class BammBammClient {
|
||||||
@ParametersAreNonnullByDefault
|
@ParametersAreNonnullByDefault
|
||||||
private static class Request {
|
private static class Request {
|
||||||
private static final HttpClient CLIENT = HttpClients.createDefault();
|
private static final HttpClient CLIENT = HttpClients.createDefault();
|
||||||
|
private static final Encoder BASE64 = Base64.getEncoder();
|
||||||
private static final Charset UTF8 = Charset.forName("utf-8");
|
private static final Charset UTF8 = Charset.forName("utf-8");
|
||||||
|
|
||||||
private final List<NameValuePair> params = new ArrayList<>();
|
private final List<NameValuePair> params = new ArrayList<>();
|
||||||
|
@ -202,6 +243,19 @@ public class BammBammClient {
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds a binary form parameter. It will be sent in the request body.
|
||||||
|
*
|
||||||
|
* @param key
|
||||||
|
* Parameter name
|
||||||
|
* @param value
|
||||||
|
* Parameter value. It will be Base64 encoded.
|
||||||
|
* @return itself
|
||||||
|
*/
|
||||||
|
public Request param(String key, byte[] value) {
|
||||||
|
return param(key, BASE64.encodeToString(value));
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Submits the POST request.
|
* Submits the POST request.
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -0,0 +1,80 @@
|
||||||
|
/*
|
||||||
|
* acme4j - Java ACME client
|
||||||
|
*
|
||||||
|
* Copyright (C) 2018 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.it;
|
||||||
|
|
||||||
|
import java.io.ByteArrayInputStream;
|
||||||
|
import java.security.KeyFactory;
|
||||||
|
import java.security.PrivateKey;
|
||||||
|
import java.security.cert.CertificateFactory;
|
||||||
|
import java.security.cert.X509Certificate;
|
||||||
|
import java.security.spec.PKCS8EncodedKeySpec;
|
||||||
|
import java.util.Base64;
|
||||||
|
import java.util.Base64.Decoder;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
import org.shredzone.acme4j.it.server.TlsAlpnServer;
|
||||||
|
|
||||||
|
import fi.iki.elonen.NanoHTTPD.IHTTPSession;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Request handler for all {@code tls-alpn-01} related requests.
|
||||||
|
*/
|
||||||
|
public final class TlsAlpnHandler {
|
||||||
|
|
||||||
|
public static final String ADD = "/tlsalpn/add/:alias";
|
||||||
|
public static final String REMOVE = "/tlsalpn/remove/:alias";
|
||||||
|
|
||||||
|
private TlsAlpnHandler() {
|
||||||
|
// this class cannot be instanciated.
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds an TLS-ALPN certificate.
|
||||||
|
*/
|
||||||
|
public static class Add extends AbstractResponder {
|
||||||
|
@Override
|
||||||
|
public void handle(Map<String, String> urlParams, IHTTPSession session) throws Exception {
|
||||||
|
String alias = urlParams.get("alias");
|
||||||
|
String privateKeyEncoded = session.getParameters().get("privateKey").get(0);
|
||||||
|
String certEncoded = session.getParameters().get("cert").get(0);
|
||||||
|
|
||||||
|
Decoder base64 = Base64.getDecoder();
|
||||||
|
|
||||||
|
KeyFactory kf = KeyFactory.getInstance("RSA");
|
||||||
|
PrivateKey privateKey = kf.generatePrivate(new PKCS8EncodedKeySpec(
|
||||||
|
base64.decode(privateKeyEncoded)));
|
||||||
|
|
||||||
|
CertificateFactory cf = CertificateFactory.getInstance("X.509");
|
||||||
|
X509Certificate cert = (X509Certificate) cf.generateCertificate(
|
||||||
|
new ByteArrayInputStream(base64.decode(certEncoded)));
|
||||||
|
|
||||||
|
TlsAlpnServer server = BammBamm.instance().getTlsAlpnServer();
|
||||||
|
server.addCertificate(alias, privateKey, cert);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes an TLS-ALPN certificate.
|
||||||
|
*/
|
||||||
|
public static class Remove extends AbstractResponder {
|
||||||
|
@Override
|
||||||
|
public void handle(Map<String, String> urlParams, IHTTPSession session) throws Exception {
|
||||||
|
String alias = urlParams.get("alias");
|
||||||
|
|
||||||
|
TlsAlpnServer server = BammBamm.instance().getTlsAlpnServer();
|
||||||
|
server.removeCertificate(alias);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,262 @@
|
||||||
|
/*
|
||||||
|
* acme4j - Java ACME client
|
||||||
|
*
|
||||||
|
* Copyright (C) 2018 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.it.server;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.security.KeyManagementException;
|
||||||
|
import java.security.KeyStore;
|
||||||
|
import java.security.KeyStoreException;
|
||||||
|
import java.security.NoSuchAlgorithmException;
|
||||||
|
import java.security.PrivateKey;
|
||||||
|
import java.security.UnrecoverableKeyException;
|
||||||
|
import java.security.cert.Certificate;
|
||||||
|
import java.security.cert.CertificateException;
|
||||||
|
import java.security.cert.CertificateParsingException;
|
||||||
|
import java.security.cert.X509Certificate;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
import javax.net.ssl.KeyManager;
|
||||||
|
import javax.net.ssl.KeyManagerFactory;
|
||||||
|
import javax.net.ssl.SSLContext;
|
||||||
|
import javax.net.ssl.SSLServerSocket;
|
||||||
|
import javax.net.ssl.SSLServerSocketFactory;
|
||||||
|
import javax.net.ssl.SSLSession;
|
||||||
|
import javax.net.ssl.SSLSocket;
|
||||||
|
import javax.net.ssl.TrustManager;
|
||||||
|
import javax.net.ssl.TrustManagerFactory;
|
||||||
|
|
||||||
|
import org.bouncycastle.asn1.x509.GeneralName;
|
||||||
|
import org.eclipse.jetty.alpn.ALPN;
|
||||||
|
import org.shredzone.acme4j.challenge.TlsAlpn01Challenge;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A very simple TLS-ALPN server. It waits for a connection and performs a TLS handshake
|
||||||
|
* returning the matching certificate to the requested domain.
|
||||||
|
* <p>
|
||||||
|
* This server can be used to validate {@code tls-alpn-01} challenges.
|
||||||
|
*/
|
||||||
|
public class TlsAlpnServer {
|
||||||
|
private static final Logger LOG = LoggerFactory.getLogger(TlsAlpnServer.class);
|
||||||
|
private static final char[] PASSWORD = "shibboleet".toCharArray();
|
||||||
|
|
||||||
|
private KeyStore keyStore = null;
|
||||||
|
private Thread thread = null;
|
||||||
|
private volatile boolean running = false;
|
||||||
|
private volatile boolean listening = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds a certificate to the set of known certificates.
|
||||||
|
* <p>
|
||||||
|
* The certificate's SAN is used for SNI.
|
||||||
|
*
|
||||||
|
* @param alias
|
||||||
|
* Internal alias
|
||||||
|
* @param privateKey
|
||||||
|
* Private key to be used with this certificate
|
||||||
|
* @param cert
|
||||||
|
* {@link X509Certificate} to be added
|
||||||
|
*/
|
||||||
|
public void addCertificate(String alias, PrivateKey privateKey, X509Certificate cert) {
|
||||||
|
initKeyStore();
|
||||||
|
|
||||||
|
try {
|
||||||
|
keyStore.setKeyEntry(alias, privateKey, PASSWORD, new Certificate[] {cert});
|
||||||
|
} catch (KeyStoreException ex) {
|
||||||
|
throw new IllegalArgumentException("Failed to add certificate " + alias, ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes a certificate.
|
||||||
|
*
|
||||||
|
* @param alias
|
||||||
|
* Internal alias of the certificate to remove
|
||||||
|
*/
|
||||||
|
public void removeCertificate(String alias) {
|
||||||
|
initKeyStore();
|
||||||
|
|
||||||
|
try {
|
||||||
|
keyStore.deleteEntry(alias);
|
||||||
|
} catch (KeyStoreException ex) {
|
||||||
|
throw new IllegalArgumentException("Failed to remove certificate " + alias, ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Starts the TlsAlpn server.
|
||||||
|
*
|
||||||
|
* @param port
|
||||||
|
* Port to listen to
|
||||||
|
*/
|
||||||
|
public void start(int port) {
|
||||||
|
if (thread != null) {
|
||||||
|
throw new IllegalStateException("Server is already running");
|
||||||
|
}
|
||||||
|
|
||||||
|
running = true;
|
||||||
|
thread = new Thread(() -> serve(port));
|
||||||
|
thread.setName("tls-alpn server");
|
||||||
|
thread.start();
|
||||||
|
LOG.info("tls-alpn server listening at port {}", port);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stops the TlsAlpn server.
|
||||||
|
*/
|
||||||
|
public void stop() {
|
||||||
|
if (thread != null) {
|
||||||
|
running = false;
|
||||||
|
thread.interrupt();
|
||||||
|
thread = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if the server was started up and is listening to connections.
|
||||||
|
*/
|
||||||
|
public boolean isListening() {
|
||||||
|
return listening;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Opens an SSL server socket and processes incoming requests.
|
||||||
|
*
|
||||||
|
* @param port
|
||||||
|
* Port to listen at
|
||||||
|
*/
|
||||||
|
private void serve(int port) {
|
||||||
|
SSLContext sslContext = createSSLContext();
|
||||||
|
SSLServerSocketFactory sslServerSocketFactory = sslContext.getServerSocketFactory();
|
||||||
|
|
||||||
|
try (SSLServerSocket sslServerSocket = (SSLServerSocket)
|
||||||
|
sslServerSocketFactory.createServerSocket(port)){
|
||||||
|
listening = true;
|
||||||
|
while (running) {
|
||||||
|
process(sslServerSocket);
|
||||||
|
}
|
||||||
|
} catch (IOException ex) {
|
||||||
|
LOG.error("Failed to create socket on port {}", port, ex);
|
||||||
|
}
|
||||||
|
|
||||||
|
listening = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Accept and process an incoming request. Only the TLS handshake is used here.
|
||||||
|
* Incoming data is just consumed, and the socket is closed after that.
|
||||||
|
*
|
||||||
|
* @param sslServerSocket
|
||||||
|
* {@link SSLServerSocket} to accept connections from
|
||||||
|
*/
|
||||||
|
private void process(SSLServerSocket sslServerSocket) {
|
||||||
|
try (SSLSocket sslSocket = (SSLSocket) sslServerSocket.accept()) {
|
||||||
|
ALPN.put(sslSocket, new ALPN.ServerProvider() {
|
||||||
|
@Override
|
||||||
|
public void unsupported() {
|
||||||
|
ALPN.remove(sslSocket);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String select(List<String> protocols) {
|
||||||
|
ALPN.remove(sslSocket);
|
||||||
|
if (protocols.contains(TlsAlpn01Challenge.ACME_TLS_1_PROTOCOL)) {
|
||||||
|
return TlsAlpn01Challenge.ACME_TLS_1_PROTOCOL;
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
sslSocket.setEnabledCipherSuites(sslSocket.getSupportedCipherSuites());
|
||||||
|
sslSocket.startHandshake();
|
||||||
|
|
||||||
|
SSLSession sslSession = sslSocket.getSession();
|
||||||
|
X509Certificate cert = (X509Certificate) sslSession.getLocalCertificates()[0];
|
||||||
|
LOG.info("tls-alpn: {}", domainsToString(cert));
|
||||||
|
|
||||||
|
try (InputStream in = sslSocket.getInputStream()) {
|
||||||
|
while (in.read() >= 0); //NOSONAR: intentional empty statement
|
||||||
|
}
|
||||||
|
} catch (Exception ex) {
|
||||||
|
LOG.error("Failed to process request", ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Lazily initializes the {@link KeyStore} instance to be used. The key store is empty
|
||||||
|
* after initialization.
|
||||||
|
*/
|
||||||
|
private void initKeyStore() {
|
||||||
|
if (keyStore == null) {
|
||||||
|
try {
|
||||||
|
keyStore = KeyStore.getInstance("JKS");
|
||||||
|
keyStore.load(null, null);
|
||||||
|
} catch (KeyStoreException | NoSuchAlgorithmException | CertificateException
|
||||||
|
| IOException ex) {
|
||||||
|
throw new IllegalStateException("Failed to create key store", ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a {@link SSLContext} that uses the internal {@link KeyStore} for key and
|
||||||
|
* trust management.
|
||||||
|
*
|
||||||
|
* @return {@link SSLContext} instance
|
||||||
|
*/
|
||||||
|
private SSLContext createSSLContext() {
|
||||||
|
initKeyStore();
|
||||||
|
|
||||||
|
try {
|
||||||
|
KeyManagerFactory keyManagerFactory = KeyManagerFactory.getInstance("NewSunX509");
|
||||||
|
keyManagerFactory.init(keyStore, PASSWORD);
|
||||||
|
KeyManager[] km = keyManagerFactory.getKeyManagers();
|
||||||
|
|
||||||
|
TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance("SunX509");
|
||||||
|
trustManagerFactory.init(keyStore);
|
||||||
|
TrustManager[] tm = trustManagerFactory.getTrustManagers();
|
||||||
|
|
||||||
|
SSLContext sslContext = SSLContext.getInstance("TLSv1");
|
||||||
|
sslContext.init(km, tm, null);
|
||||||
|
|
||||||
|
return sslContext;
|
||||||
|
} catch (KeyManagementException | NoSuchAlgorithmException | KeyStoreException
|
||||||
|
| UnrecoverableKeyException ex) {
|
||||||
|
throw new IllegalStateException("Could not create SSLContext", ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extracts all SANs of the given certificate and returns them as a string.
|
||||||
|
*
|
||||||
|
* @param cert
|
||||||
|
* {@link X509Certificate} to read the SANs from
|
||||||
|
* @return String of all SAN names joined together and separated by comma
|
||||||
|
*/
|
||||||
|
private String domainsToString(X509Certificate cert) {
|
||||||
|
try {
|
||||||
|
return cert.getSubjectAlternativeNames().stream()
|
||||||
|
.filter(c -> ((Number) c.get(0)).intValue() == GeneralName.dNSName)
|
||||||
|
.map(c -> (String) c.get(1))
|
||||||
|
.collect(Collectors.joining(", "));
|
||||||
|
} catch (CertificateParsingException ex) {
|
||||||
|
throw new IllegalArgumentException("bad certificate", ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -7,3 +7,6 @@ dns.port = 53
|
||||||
|
|
||||||
# HTTP server port
|
# HTTP server port
|
||||||
http.port = 5002
|
http.port = 5002
|
||||||
|
|
||||||
|
# TLS-ALPN server port
|
||||||
|
tlsAlpn.port = 5001
|
||||||
|
|
|
@ -35,8 +35,10 @@ import org.shredzone.acme4j.Status;
|
||||||
import org.shredzone.acme4j.challenge.Challenge;
|
import org.shredzone.acme4j.challenge.Challenge;
|
||||||
import org.shredzone.acme4j.challenge.Dns01Challenge;
|
import org.shredzone.acme4j.challenge.Dns01Challenge;
|
||||||
import org.shredzone.acme4j.challenge.Http01Challenge;
|
import org.shredzone.acme4j.challenge.Http01Challenge;
|
||||||
|
import org.shredzone.acme4j.challenge.TlsAlpn01Challenge;
|
||||||
import org.shredzone.acme4j.it.BammBammClient;
|
import org.shredzone.acme4j.it.BammBammClient;
|
||||||
import org.shredzone.acme4j.util.CSRBuilder;
|
import org.shredzone.acme4j.util.CSRBuilder;
|
||||||
|
import org.shredzone.acme4j.util.CertificateUtils;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Tests a complete certificate order with different challenges.
|
* Tests a complete certificate order with different challenges.
|
||||||
|
@ -87,6 +89,32 @@ public class OrderIT extends PebbleITBase {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test if a certificate can be ordered via tns-alpn-01 challenge.
|
||||||
|
*/
|
||||||
|
@Test
|
||||||
|
public void testTlsAlpnValidation() throws Exception {
|
||||||
|
orderCertificate(TEST_DOMAIN, auth -> {
|
||||||
|
BammBammClient client = getBammBammClient();
|
||||||
|
|
||||||
|
TlsAlpn01Challenge challenge = auth.findChallenge(TlsAlpn01Challenge.TYPE);
|
||||||
|
assertThat(challenge, is(notNullValue()));
|
||||||
|
|
||||||
|
KeyPair challengeKey = createKeyPair();
|
||||||
|
|
||||||
|
X509Certificate cert = CertificateUtils.createTlsAlpn01Certificate(
|
||||||
|
challengeKey, auth.getDomain(), challenge.getAcmeValidationV1());
|
||||||
|
|
||||||
|
client.dnsAddARecord(TEST_DOMAIN, getBammBammHostname());
|
||||||
|
client.tlsAlpnAddCertificate(auth.getDomain(), challengeKey.getPrivate(), cert);
|
||||||
|
|
||||||
|
cleanup(() -> client.dnsRemoveARecord(TEST_DOMAIN));
|
||||||
|
cleanup(() -> client.tlsAlpnRemoveCertificate(auth.getDomain()));
|
||||||
|
|
||||||
|
return challenge;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Runs the complete process of ordering a certificate.
|
* Runs the complete process of ordering a certificate.
|
||||||
*
|
*
|
||||||
|
|
|
@ -3,6 +3,7 @@
|
||||||
"listenAddress": "0.0.0.0:14000",
|
"listenAddress": "0.0.0.0:14000",
|
||||||
"certificate": "/go/src/github.com/letsencrypt/pebble/test/certs/localhost/cert.pem",
|
"certificate": "/go/src/github.com/letsencrypt/pebble/test/certs/localhost/cert.pem",
|
||||||
"privateKey": "/go/src/github.com/letsencrypt/pebble/test/certs/localhost/key.pem",
|
"privateKey": "/go/src/github.com/letsencrypt/pebble/test/certs/localhost/key.pem",
|
||||||
"httpPort": 5002
|
"httpPort": 5002,
|
||||||
|
"tlsPort": 5001
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,15 +14,16 @@ The `acme4j-it` module contains a small and very simple test server called _Bamm
|
||||||
|
|
||||||
* 14001: Provides a simple REST-like interface for adding and removing challenge tokens and DNS records. This port is exposed.
|
* 14001: Provides a simple REST-like interface for adding and removing challenge tokens and DNS records. This port is exposed.
|
||||||
* 53 (UDP): A simple DNS server for resolving test domain names, and providing `TXT` records for `dns-01` challenges.
|
* 53 (UDP): A simple DNS server for resolving test domain names, and providing `TXT` records for `dns-01` challenges.
|
||||||
* 5002: A simple HTTP server that responses with tokens for `http-01` challenges.
|
* 5001: A simple TLS-ALPN server that responds with certificates for `tls-alpn-01` challenges.
|
||||||
|
* 5002: A simple HTTP server that responds with tokens for `http-01` challenges.
|
||||||
|
|
||||||
To run this server, you can use the Docker image mentioned above. You could also run the server directly, but since the DNS server is listening on a privileged port, it would need to be reconfigured first.
|
To run this server, you can use the Docker image mentioned above. You could also run the server directly, but since the DNS server is listening on a privileged port, it would need to be reconfigured first. Also, if you run BammBamm with OpenJDK 8, make sure to [add the correct `alpn-boot.jar` version to your boot classpath](https://www.eclipse.org/jetty/documentation/9.4.x/alpn-chapter.html#alpn-versions) in order to use the TLS-ALPN server.
|
||||||
|
|
||||||
The `BammBammClient` class can be used to set the challenge responses and DNS records via the REST interface on port 14001.
|
The `BammBammClient` class can be used to set the challenge responses and DNS records via the REST interface on port 14001.
|
||||||
|
|
||||||
<div class="alert alert-danger" role="alert">
|
<div class="alert alert-danger" role="alert">
|
||||||
|
|
||||||
Do not use _Bammbamm_ in production environments! It has its main focus on simplicity, and is only meant as a server for integration test purposes. It is neither hardened, nor feature complete.
|
Do not use _BammBamm_ in production environments! It has its main focus on simplicity, and is only meant as a server for integration test purposes. It is neither hardened, nor feature complete.
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
## Boulder
|
## Boulder
|
||||||
|
@ -39,4 +40,4 @@ Now set up a Docker instance of Boulder. Follow the instructions in the [Boulder
|
||||||
|
|
||||||
The Boulder integration tests can now be run with `mvn -P boulder verify`.
|
The Boulder integration tests can now be run with `mvn -P boulder verify`.
|
||||||
|
|
||||||
For a local Boulder installation, just make sure that `FAKE_DNS` is set to `127.0.0.1`. You'll also need to expose the port 5002 of _BammBamm_ by changing the `acme4j-it/pom.xml` accordingly.
|
For a local Boulder installation, just make sure that `FAKE_DNS` is set to `127.0.0.1`. You'll also need to expose the ports 5001 and 5002 of _BammBamm_ by changing the `acme4j-it/pom.xml` accordingly.
|
||||||
|
|
Loading…
Reference in New Issue