Remove tls-sni-02 challenge

pull/61/head
Richard Körber 2018-01-14 16:53:02 +01:00
parent 472f1497db
commit 137c2c7dd0
No known key found for this signature in database
GPG Key ID: AAB9FD19C78AA3E0
24 changed files with 6 additions and 983 deletions

View File

@ -1,67 +0,0 @@
/*
* acme4j - Java ACME client
*
* Copyright (C) 2016 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.challenge;
import static org.shredzone.acme4j.toolbox.AcmeUtils.*;
import org.shredzone.acme4j.Session;
import org.shredzone.acme4j.toolbox.JSON;
/**
* Implements the {@value TYPE} challenge.
*/
public class TlsSni02Challenge extends TokenChallenge {
private static final long serialVersionUID = 8921833167878544518L;
/**
* Challenge type name: {@value}
*/
public static final String TYPE = "tls-sni-02";
/**
* Creates a new generic {@link TlsSni02Challenge} object.
*
* @param session
* {@link Session} to bind to.
* @param data
* {@link JSON} challenge data
*/
public TlsSni02Challenge(Session session, JSON data) {
super(session, data);
}
/**
* Returns the subject, which is to be used as "SAN-A" in a self-signed certificate.
* The CA will send the SNI request against this domain.
*/
public String getSubject() {
String tokenHash = hexEncode(sha256hash(getToken()));
return tokenHash.substring(0, 32) + '.' + tokenHash.substring(32) + ".token.acme.invalid";
}
/**
* Returns the key authorization, which is to be used as "SAN-B" in a self-signed
* certificate.
*/
public String getSanB() {
String kaHash = hexEncode(sha256hash(getAuthorization()));
return kaHash.substring(0, 32) + '.' + kaHash.substring(32) + ".ka.acme.invalid";
}
@Override
protected boolean acceptable(String type) {
return TYPE.equals(type);
}
}

View File

@ -24,7 +24,6 @@ import org.shredzone.acme4j.Session;
import org.shredzone.acme4j.challenge.Challenge;
import org.shredzone.acme4j.challenge.Dns01Challenge;
import org.shredzone.acme4j.challenge.Http01Challenge;
import org.shredzone.acme4j.challenge.TlsSni02Challenge;
import org.shredzone.acme4j.challenge.TokenChallenge;
import org.shredzone.acme4j.connector.Connection;
import org.shredzone.acme4j.connector.DefaultConnection;
@ -64,7 +63,6 @@ public abstract class AbstractAcmeProvider implements AcmeProvider {
Map<String, BiFunction<Session, JSON, Challenge>> map = new HashMap<>();
map.put(Dns01Challenge.TYPE, Dns01Challenge::new);
map.put(TlsSni02Challenge.TYPE, TlsSni02Challenge::new);
map.put(Http01Challenge.TYPE, Http01Challenge::new);
return Collections.unmodifiableMap(map);

View File

@ -29,7 +29,6 @@ import org.junit.Test;
import org.shredzone.acme4j.challenge.Challenge;
import org.shredzone.acme4j.challenge.Dns01Challenge;
import org.shredzone.acme4j.challenge.Http01Challenge;
import org.shredzone.acme4j.challenge.TlsSni02Challenge;
import org.shredzone.acme4j.exception.AcmeException;
import org.shredzone.acme4j.exception.AcmeProtocolException;
import org.shredzone.acme4j.exception.AcmeRetryAfterException;
@ -67,11 +66,6 @@ public class AuthorizationTest {
Challenge c3 = authorization.findChallenge(Dns01Challenge.TYPE);
assertThat(c3, is(notNullValue()));
assertThat(c3, is(instanceOf(Dns01Challenge.class)));
// TlsSni02Challenge is available
Challenge c4 = authorization.findChallenge(TlsSni02Challenge.TYPE);
assertThat(c4, is(notNullValue()));
assertThat(c4, is(instanceOf(TlsSni02Challenge.class)));
}
/**
@ -265,7 +259,6 @@ public class AuthorizationTest {
provider.putTestChallenge(Http01Challenge.TYPE, Http01Challenge::new);
provider.putTestChallenge(Dns01Challenge.TYPE, Dns01Challenge::new);
provider.putTestChallenge(TlsSni02Challenge.TYPE, TlsSni02Challenge::new);
provider.putTestChallenge(DUPLICATE_TYPE, Challenge::new);
Authorization authorization = new Authorization(session, locationUrl);

View File

@ -1,63 +0,0 @@
/*
* acme4j - Java ACME client
*
* Copyright (C) 2016 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.challenge;
import static org.hamcrest.Matchers.is;
import static org.junit.Assert.assertThat;
import static org.shredzone.acme4j.toolbox.TestUtils.getJSON;
import static uk.co.datumedge.hamcrest.json.SameJSONAs.sameJSONAs;
import java.io.IOException;
import org.junit.BeforeClass;
import org.junit.Test;
import org.shredzone.acme4j.Session;
import org.shredzone.acme4j.Status;
import org.shredzone.acme4j.toolbox.JSONBuilder;
import org.shredzone.acme4j.toolbox.TestUtils;
/**
* Unit tests for {@link TlsSni02Challenge}.
*/
public class TlsSni02ChallengeTest {
private static final String KEY_AUTHORIZATION =
"VNLBdSiZ3LppU2CRG8bilqlwq4DuApJMg3ZJowU6JhQ.HnWjTDnyqlCrm6tZ-6wX-TrEXgRdeNu9G71gqxSO6o0";
private static Session session;
@BeforeClass
public static void setup() throws IOException {
session = TestUtils.session();
}
/**
* Test that {@link TlsSni02Challenge} generates a correct authorization key.
*/
@Test
public void testTlsSni02Challenge() throws IOException {
TlsSni02Challenge challenge = new TlsSni02Challenge(session, getJSON("tlsSni02Challenge"));
assertThat(challenge.getType(), is(TlsSni02Challenge.TYPE));
assertThat(challenge.getStatus(), is(Status.PENDING));
assertThat(challenge.getSubject(), is("5bf0b9908ed73bc53ed3327afa52f76b.0a4bea00520f0753f42abe0bb39e3ea8.token.acme.invalid"));
assertThat(challenge.getSanB(), is("14e2350a04434f93c2e0b6012968d99d.ed459b6a7a019d9695609b8514f9d63d.ka.acme.invalid"));
JSONBuilder response = new JSONBuilder();
challenge.prepareResponse(response);
assertThat(response.toString(), sameJSONAs("{\"keyAuthorization\"=\""
+ KEY_AUTHORIZATION + "\"}").allowingExtraUnexpectedFields());
}
}

View File

@ -29,7 +29,6 @@ import org.shredzone.acme4j.Session;
import org.shredzone.acme4j.challenge.Challenge;
import org.shredzone.acme4j.challenge.Dns01Challenge;
import org.shredzone.acme4j.challenge.Http01Challenge;
import org.shredzone.acme4j.challenge.TlsSni02Challenge;
import org.shredzone.acme4j.challenge.TokenChallenge;
import org.shredzone.acme4j.connector.Connection;
import org.shredzone.acme4j.connector.DefaultConnection;
@ -147,10 +146,6 @@ public class AbstractAcmeProviderTest {
assertThat(c3, not(nullValue()));
assertThat(c3, instanceOf(Dns01Challenge.class));
Challenge c5 = provider.createChallenge(session, getJSON("tlsSni02Challenge"));
assertThat(c5, not(nullValue()));
assertThat(c5, instanceOf(TlsSni02Challenge.class));
JSON json6 = new JSONBuilder()
.put("type", "foobar-01")
.put("url", "https://example.com/some/challenge")

View File

@ -10,11 +10,6 @@
"url": "https://example.com/authz/asdf/1",
"token": "DGyRejmCefe7v4NfDGDKfA"
},
{
"type": "tls-sni-02",
"url": "https://example.com/authz/asdf/2",
"token": "VNLBdSiZ3LppU2CRG8bilqlwq4DuApJMg3ZJowU6JhQ"
},
{
"type": "duplicate-01",
"url": "https://example.com/authz/asdf/3"

View File

@ -1,6 +0,0 @@
{
"type": "tls-sni-02",
"url": "https://example.com/acme/authz/0",
"status": "pending",
"token": "VNLBdSiZ3LppU2CRG8bilqlwq4DuApJMg3ZJowU6JhQ"
}

View File

@ -21,21 +21,17 @@ import java.io.Writer;
import java.net.URI;
import java.security.KeyPair;
import java.security.Security;
import java.security.cert.X509Certificate;
import java.util.Arrays;
import java.util.Collection;
import javax.swing.JOptionPane;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.bouncycastle.openssl.jcajce.JcaPEMWriter;
import org.shredzone.acme4j.challenge.Challenge;
import org.shredzone.acme4j.challenge.Dns01Challenge;
import org.shredzone.acme4j.challenge.Http01Challenge;
import org.shredzone.acme4j.challenge.TlsSni02Challenge;
import org.shredzone.acme4j.exception.AcmeException;
import org.shredzone.acme4j.util.CSRBuilder;
import org.shredzone.acme4j.util.CertificateUtils;
import org.shredzone.acme4j.util.KeyPairUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@ -66,7 +62,7 @@ public class ClientTest {
private static final Logger LOG = LoggerFactory.getLogger(ClientTest.class);
private enum ChallengeType { HTTP, DNS, TLSSNI }
private enum ChallengeType { HTTP, DNS }
/**
* Generates a certificate for the given domains. Also takes care for the registration
@ -218,10 +214,6 @@ public class ClientTest {
case DNS:
challenge = dnsChallenge(auth);
break;
case TLSSNI:
challenge = tlsSniChallenge(auth);
break;
}
if (challenge == null) {
@ -334,62 +326,6 @@ public class ClientTest {
return challenge;
}
/**
* Prepares a TLS-SNI challenge.
* <p>
* The verification of this challenge expects that the web server returns a special
* validation certificate.
* <p>
* This example outputs instructions that need to be executed manually. In a
* production environment, you would rather configure your web server automatically.
*
* @param auth
* {@link Authorization} to find the challenge in
* @return {@link Challenge} to verify
*/
public Challenge tlsSniChallenge(Authorization auth) throws AcmeException {
// Find a single tls-sni-02 challenge
TlsSni02Challenge challenge = auth.findChallenge(TlsSni02Challenge.TYPE);
if (challenge == null) {
throw new AcmeException("Found no " + TlsSni02Challenge.TYPE + " challenge, don't know what to do...");
}
// Get the Subject
String subject = challenge.getSubject();
String sanB = challenge.getSanB();
// Create a validation key pair
KeyPair domainKeyPair;
try (FileWriter fw = new FileWriter("tlssni.key")) {
domainKeyPair = KeyPairUtils.createKeyPair(2048);
KeyPairUtils.writeKeyPair(domainKeyPair, fw);
} catch (IOException ex) {
throw new AcmeException("Could not write keypair", ex);
}
// Create a validation certificate
try (JcaPEMWriter pw = new JcaPEMWriter(new FileWriter("tlssni.crt"))) {
X509Certificate cert = CertificateUtils.createTlsSni02Certificate(domainKeyPair, subject, sanB);
pw.writeObject(cert);
} catch (IOException ex) {
throw new AcmeException("Could not write certificate", ex);
}
// Output the challenge, wait for acknowledge...
LOG.info("Please configure your web server.");
LOG.info("It must return the certificate 'tlssni.crt' on a SNI request to:");
LOG.info(subject);
LOG.info("The matching keypair is available at 'tlssni.key'.");
LOG.info("If you're ready, dismiss the dialog...");
StringBuilder message = new StringBuilder();
message.append("Please use 'tlssni.key' and 'tlssni.crt' cert for SNI requests to:\n\n");
message.append("https://").append(subject).append("\n\n");
acceptChallenge(message.toString());
return challenge;
}
/**
* Presents the instructions for preparing the challenge validation, and waits for
* dismissal. If the user cancelled the dialog, an exception is thrown.

View File

@ -178,7 +178,6 @@
<from>openjdk:8-jre</from>
<ports>
<port>53/udp</port>
<port>5001</port>
<port>5002</port>
<port>14001</port>
</ports>

View File

@ -19,7 +19,6 @@ import java.util.ResourceBundle;
import org.shredzone.acme4j.it.server.DnsServer;
import org.shredzone.acme4j.it.server.HttpServer;
import org.shredzone.acme4j.it.server.TlsSniServer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@ -27,8 +26,8 @@ import fi.iki.elonen.NanoHTTPD;
import fi.iki.elonen.router.RouterNanoHTTPD;
/**
* A mock server to test Pebble. It provides a HTTP server, TLS-SNI server, and DNS
* server. The servers can be configured remotely via simple HTTP POST requests.
* A mock server to test Pebble. It provides a HTTP server and DNS server. The servers can
* be configured remotely via simple HTTP POST requests.
* <p>
* <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!
@ -41,22 +40,18 @@ public class BammBamm {
private final int appPort;
private final int httpPort;
private final int dnsPort;
private final int tlsSniPort;
private final AppServer appServer;
private final DnsServer dnsServer;
private final HttpServer httpServer;
private final TlsSniServer tlsSniServer;
private BammBamm() {
ResourceBundle bundle = ResourceBundle.getBundle("bammbamm");
appPort = Integer.parseInt(bundle.getString("app.port"));
dnsPort = Integer.parseInt(bundle.getString("dns.port"));
httpPort = Integer.parseInt(bundle.getString("http.port"));
tlsSniPort = Integer.parseInt(bundle.getString("tlsSni.port"));
dnsServer = new DnsServer();
httpServer = new HttpServer();
tlsSniServer = new TlsSniServer();
appServer = new AppServer(appPort);
}
@ -83,20 +78,12 @@ public class BammBamm {
return httpServer;
}
/**
* Returns the {@link TlsSniServer} instance.
*/
public TlsSniServer getTlsSniServer() {
return tlsSniServer;
}
/**
* Starts the servers.
*/
public void start() {
dnsServer.start(dnsPort);
httpServer.start(httpPort);
tlsSniServer.start(tlsSniPort);
try {
appServer.start(NanoHTTPD.SOCKET_READ_TIMEOUT, true);
@ -112,7 +99,6 @@ public class BammBamm {
*/
public void stop() {
appServer.stop();
tlsSniServer.stop();
httpServer.stop();
dnsServer.stop();
@ -134,9 +120,6 @@ public class BammBamm {
addRoute(HttpHandler.ADD, HttpHandler.Add.class);
addRoute(HttpHandler.REMOVE, HttpHandler.Remove.class);
addRoute(TlsSniHandler.ADD, TlsSniHandler.Add.class);
addRoute(TlsSniHandler.REMOVE, TlsSniHandler.Remove.class);
}
}

View File

@ -17,12 +17,7 @@ import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
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.Base64;
import java.util.Base64.Encoder;
import java.util.List;
import org.apache.http.HttpResponse;
@ -136,41 +131,6 @@ public class BammBammClient {
.submit();
}
/**
* Adds a certificate for TLS-SNI 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 names to respond to
*/
public void tlsSniAddCertificate(String alias, PrivateKey privateKey, X509Certificate cert) throws IOException {
try {
createRequest(TlsSniHandler.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 tlsSniRemoveCertificate(String alias) throws IOException {
createRequest(TlsSniHandler.REMOVE)
.arg(":alias", alias)
.submit();
}
/**
* Creates a new {@link Request} object.
*
@ -187,7 +147,6 @@ public class BammBammClient {
*/
private static class Request {
private static final HttpClient CLIENT = HttpClients.createDefault();
private static final Encoder BASE64 = Base64.getEncoder();
private static final Charset UTF8 = Charset.forName("utf-8");
private final List<NameValuePair> params = new ArrayList<>();
@ -239,19 +198,6 @@ public class BammBammClient {
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.
*/

View File

@ -1,80 +0,0 @@
/*
* 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.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.TlsSniServer;
import fi.iki.elonen.NanoHTTPD.IHTTPSession;
/**
* Request handler for all {@code tls-sni-02} related requests.
*/
public final class TlsSniHandler {
public static final String ADD = "/tlssni/add/:alias";
public static final String REMOVE = "/tlssni/remove/:alias";
private TlsSniHandler() {
// this class cannot be instanciated.
}
/**
* Adds an TLS-SNI 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)));
TlsSniServer server = BammBamm.instance().getTlsSniServer();
server.addCertificate(alias, privateKey, cert);
}
}
/**
* Removes an TLS-SNI certificate.
*/
public static class Remove extends AbstractResponder {
@Override
public void handle(Map<String, String> urlParams, IHTTPSession session) throws Exception {
String alias = urlParams.get("alias");
TlsSniServer server = BammBamm.instance().getTlsSniServer();
server.removeCertificate(alias);
}
}
}

View File

@ -1,242 +0,0 @@
/*
* 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.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.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.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* A very simple TnsSni 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-sni-02} challenges.
*/
public class TlsSniServer {
private static final Logger LOG = LoggerFactory.getLogger(TlsSniServer.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 CN and SANs are 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 TlsSni 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-sni server");
thread.start();
LOG.info("tls-sni server listening at port {}", port);
}
/**
* Stops the TlsSni 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()) {
sslSocket.setEnabledCipherSuites(sslSocket.getSupportedCipherSuites());
sslSocket.startHandshake();
SSLSession sslSession = sslSocket.getSession();
X509Certificate cert = (X509Certificate) sslSession.getLocalCertificates()[0];
LOG.info("tls-sni: {}", 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);
}
}
}

View File

@ -7,6 +7,3 @@ dns.port = 53
# HTTP server port
http.port = 5002
# TLS-SNI server port
tlsSni.port = 5001

View File

@ -57,6 +57,6 @@ The tags `maven` and `docker` are used to select the executor.
`acme4j-it` API
---------------
The `acme4j-it` module provides test servers for the `http-01`, `dns-01` and `tls-sni-02` challenges. You can use these classes for your own projects. However, they are not part of the official _acme4j_ API and subject to change without notice.
The `acme4j-it` module provides test servers for the `http-01` and `dns-01` challenges. You can use these classes for your own projects. However, they are not part of the official _acme4j_ API and subject to change without notice.
Note that these servers are very simple implementations without any security measures. They are tailor-made for integration tests. Do not use them in production code!

View File

@ -1,157 +0,0 @@
/*
* 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.it.boulder;
import static java.util.concurrent.TimeUnit.SECONDS;
import static org.awaitility.Awaitility.await;
import static org.hamcrest.Matchers.*;
import static org.junit.Assert.*;
import java.net.URI;
import java.security.KeyPair;
import java.security.cert.X509Certificate;
import org.junit.Test;
import org.shredzone.acme4j.Account;
import org.shredzone.acme4j.AccountBuilder;
import org.shredzone.acme4j.Authorization;
import org.shredzone.acme4j.Certificate;
import org.shredzone.acme4j.Order;
import org.shredzone.acme4j.Session;
import org.shredzone.acme4j.Status;
import org.shredzone.acme4j.challenge.TlsSni02Challenge;
import org.shredzone.acme4j.exception.AcmeException;
import org.shredzone.acme4j.exception.AcmeLazyLoadingException;
import org.shredzone.acme4j.it.BammBammClient;
import org.shredzone.acme4j.util.CSRBuilder;
import org.shredzone.acme4j.util.CertificateUtils;
import org.shredzone.acme4j.util.KeyPairUtils;
/**
* Tests a complete certificate order with different challenges.
*/
public class OrderTlsSniIT {
private static final String TEST_DOMAIN = "example.com";
private final String bammbammUrl = System.getProperty("bammbammUrl", "http://localhost:14001");
private BammBammClient client = new BammBammClient(bammbammUrl);
/**
* Test if a certificate can be ordered via http-01 challenge.
*/
@Test
public void testHttpValidation() throws Exception {
KeyPair keyPair = createKeyPair();
Session session = new Session(boulderURI(), keyPair);
Account account = new AccountBuilder()
.agreeToTermsOfService()
.create(session);
KeyPair domainKeyPair = createKeyPair();
Order order = account.newOrder().domain(TEST_DOMAIN).create();
for (Authorization auth : order.getAuthorizations()) {
TlsSni02Challenge challenge = auth.findChallenge(TlsSni02Challenge.TYPE);
assertThat(challenge, is(notNullValue()));
KeyPair challengeKeyPair = createKeyPair();
X509Certificate challengeCert = CertificateUtils.createTlsSni02Certificate(challengeKeyPair, challenge.getSubject(), challenge.getSanB());
client.tlsSniAddCertificate(challenge.getSubject(), challengeKeyPair.getPrivate(), challengeCert);
challenge.trigger();
await()
.pollInterval(1, SECONDS)
.timeout(30, SECONDS)
.conditionEvaluationListener(cond -> updateAuth(auth))
.until(auth::getStatus, not(isOneOf(Status.PENDING, Status.PROCESSING)));
if (auth.getStatus() != Status.VALID) {
fail("Authorization failed");
}
client.tlsSniRemoveCertificate(challenge.getSubject());
}
CSRBuilder csr = new CSRBuilder();
csr.addDomain(TEST_DOMAIN);
csr.sign(domainKeyPair);
byte[] encodedCsr = csr.getEncoded();
order.execute(encodedCsr);
await()
.pollInterval(1, SECONDS)
.timeout(30, SECONDS)
.conditionEvaluationListener(cond -> updateOrder(order))
.until(order::getStatus, not(isOneOf(Status.PENDING, Status.PROCESSING)));
Certificate certificate = order.getCertificate();
X509Certificate cert = certificate.getCertificate();
assertThat(cert, not(nullValue()));
assertThat(cert.getNotAfter(), not(nullValue()));
assertThat(cert.getNotBefore(), not(nullValue()));
assertThat(cert.getSubjectX500Principal().getName(), containsString("CN=" + TEST_DOMAIN));
}
/**
* @return The {@link URI} of the Boulder server to test against.
*/
protected URI boulderURI() {
return URI.create("http://localhost:4001/directory");
}
/**
* Creates a fresh key pair.
*
* @return Created new {@link KeyPair}
*/
protected KeyPair createKeyPair() {
return KeyPairUtils.createKeyPair(2048);
}
/**
* Safely updates the authorization, catching checked exceptions.
*
* @param auth
* {@link Authorization} to update
*/
private void updateAuth(Authorization auth) {
try {
auth.update();
} catch (AcmeException ex) {
throw new AcmeLazyLoadingException(auth, ex);
}
}
/**
* Safely updates the order, catching checked exceptions.
*
* @param order
* {@link Order} to update
*/
private void updateOrder(Order order) {
try {
order.update();
} catch (AcmeException ex) {
throw new AcmeLazyLoadingException(order, ex);
}
}
}

View File

@ -35,10 +35,8 @@ import org.shredzone.acme4j.Status;
import org.shredzone.acme4j.challenge.Challenge;
import org.shredzone.acme4j.challenge.Dns01Challenge;
import org.shredzone.acme4j.challenge.Http01Challenge;
import org.shredzone.acme4j.challenge.TlsSni02Challenge;
import org.shredzone.acme4j.it.BammBammClient;
import org.shredzone.acme4j.util.CSRBuilder;
import org.shredzone.acme4j.util.CertificateUtils;
/**
* Tests a complete certificate order with different challenges.
@ -47,32 +45,6 @@ public class OrderIT extends PebbleITBase {
private static final String TEST_DOMAIN = "example.com";
/**
* Test if a certificate can be ordered via tns-sni-02 challenge.
*/
@Test
public void testTlsSniValidation() throws Exception {
orderCertificate(TEST_DOMAIN, auth -> {
BammBammClient client = getBammBammClient();
TlsSni02Challenge challenge = auth.findChallenge(TlsSni02Challenge.TYPE);
assertThat(challenge, is(notNullValue()));
KeyPair challengeKey = createKeyPair();
X509Certificate cert = CertificateUtils.createTlsSni02Certificate(
challengeKey, challenge.getSubject(), challenge.getSanB());
client.dnsAddARecord(TEST_DOMAIN, getBammBammHostname());
client.tlsSniAddCertificate(challenge.getSubject(), challengeKey.getPrivate(), cert);
cleanup(() -> client.dnsRemoveARecord(TEST_DOMAIN));
cleanup(() -> client.tlsSniRemoveCertificate(challenge.getSubject()));
return challenge;
});
}
/**
* Test if a certificate can be ordered via http-01 challenge.
*/

View File

@ -3,7 +3,6 @@
"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
"httpPort": 5002
}
}

View File

@ -13,29 +13,12 @@
*/
package org.shredzone.acme4j.util;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.math.BigInteger;
import java.security.KeyPair;
import java.security.cert.CertificateException;
import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;
import java.time.Duration;
import java.time.Instant;
import java.util.Date;
import org.bouncycastle.asn1.x500.X500Name;
import org.bouncycastle.asn1.x509.Extension;
import org.bouncycastle.asn1.x509.GeneralName;
import org.bouncycastle.asn1.x509.GeneralNames;
import org.bouncycastle.cert.jcajce.JcaX509v3CertificateBuilder;
import org.bouncycastle.openssl.PEMParser;
import org.bouncycastle.operator.OperatorCreationException;
import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder;
import org.bouncycastle.pkcs.PKCS10CertificationRequest;
import org.shredzone.acme4j.challenge.TlsSni02Challenge;
/**
* Utility class offering convenience methods for certificates.
@ -66,63 +49,4 @@ public final class CertificateUtils {
}
}
/**
* Creates a self-signed {@link X509Certificate} that can be used for
* {@link TlsSni02Challenge}. The certificate is valid for 7 days.
*
* @param keypair
* A domain {@link KeyPair} to be used for the challenge
* @param sanA
* SAN-A to be used in the certificate
* @param sanB
* SAN-B to be used in the certificate
* @return Created certificate
*/
public static X509Certificate createTlsSni02Certificate(KeyPair keypair, String sanA, String sanB)
throws IOException {
return createCertificate(keypair, sanA, sanB);
}
/**
* Creates a generic self-signed challenge {@link X509Certificate}. The certificate is
* valid for 7 days.
*
* @param keypair
* A domain {@link KeyPair} to be used for the challenge
* @param subject
* Subjects to create a certificate for
* @return Created certificate
*/
private static X509Certificate createCertificate(KeyPair keypair, String... subject) throws IOException {
final long now = System.currentTimeMillis();
final String signatureAlg = "SHA256withRSA";
try {
X500Name issuer = new X500Name("CN=acme.invalid");
BigInteger serial = BigInteger.valueOf(now);
Instant notBefore = Instant.ofEpochMilli(now);
Instant notAfter = notBefore.plus(Duration.ofDays(7));
JcaX509v3CertificateBuilder certBuilder = new JcaX509v3CertificateBuilder(
issuer, serial, Date.from(notBefore), Date.from(notAfter),
issuer, keypair.getPublic());
GeneralName[] gns = new GeneralName[subject.length];
for (int ix = 0; ix < subject.length; ix++) {
gns[ix] = new GeneralName(GeneralName.dNSName, subject[ix]);
}
certBuilder.addExtension(Extension.subjectAlternativeName, false, new GeneralNames(gns));
JcaContentSignerBuilder signerBuilder = new JcaContentSignerBuilder(signatureAlg);
byte[] cert = certBuilder.build(signerBuilder.build(keypair.getPrivate())).getEncoded();
CertificateFactory certificateFactory = CertificateFactory.getInstance("X.509");
return (X509Certificate) certificateFactory.generateCertificate(new ByteArrayInputStream(cert));
} catch (CertificateException | OperatorCreationException ex) {
throw new IOException(ex);
}
}
}

View File

@ -23,16 +23,7 @@ import java.io.InputStream;
import java.lang.reflect.Constructor;
import java.lang.reflect.Modifier;
import java.security.KeyPair;
import java.security.cert.CertificateParsingException;
import java.security.cert.X509Certificate;
import java.time.Duration;
import java.time.Instant;
import java.util.Date;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import org.bouncycastle.asn1.x509.GeneralName;
import org.bouncycastle.pkcs.PKCS10CertificationRequest;
import org.junit.Test;
@ -41,30 +32,6 @@ import org.junit.Test;
*/
public class CertificateUtilsTest {
/**
* Test if {@link CertificateUtils#createTlsSni02Certificate(KeyPair, String, String)}
* creates a good certificate.
*/
@Test
public void testCreateTlsSni02Certificate() throws IOException, CertificateParsingException {
String sanA = "1082909237a535173c8415a44539f84e.248317530d8d1a0c71de8fd23f1beae4.token.acme.invalid";
String sanB = "edc3a1d40199c1723358d57853bc23ff.4d4473417a6d76e80df17bbcfbe53d2c.ka.acme.invalid";
KeyPair keypair = KeyPairUtils.createKeyPair(2048);
X509Certificate cert = CertificateUtils.createTlsSni02Certificate(keypair, sanA, sanB);
Instant now = Instant.now();
Instant end = now.plus(Duration.ofDays(8));
assertThat(cert, not(nullValue()));
assertThat(cert.getNotAfter(), is(greaterThan(Date.from(now))));
assertThat(cert.getNotAfter(), is(lessThan(Date.from(end))));
assertThat(cert.getNotBefore(), is(lessThanOrEqualTo(Date.from(now))));
assertThat(cert.getSubjectX500Principal().getName(), is("CN=acme.invalid"));
assertThat(getSANs(cert), containsInAnyOrder(sanA, sanB));
}
/**
* Test if {@link CertificateUtils#readCSR(InputStream)} reads an identical CSR.
*/
@ -100,23 +67,4 @@ public class CertificateUtilsTest {
constructor.newInstance();
}
/**
* Extracts all DNSName SANs from a certificate.
*
* @param cert
* {@link X509Certificate}
* @return Set of DNSName
*/
private Set<String> getSANs(X509Certificate cert) throws CertificateParsingException {
Set<String> result = new HashSet<>();
for (List<?> list : cert.getSubjectAlternativeNames()) {
if (((Number) list.get(0)).intValue() == GeneralName.dNSName) {
result.add((String) list.get(1));
}
}
return result;
}
}

View File

@ -10,4 +10,3 @@ The ACME specifications define these standard challenges:
* [http-01](./http-01.html)
* [dns-01](./dns-01.html)
* [tls-sni-02](./tls-sni-02.html)

View File

@ -1,44 +0,0 @@
# tls-sni-02 Challenge
With the `tls-sni-02` challenge, you prove to the CA that you are able to control the web server of the domain to be authorized, by letting it respond to a SNI request with a specific self-signed cert.
`TlsSni02Challenge` provides a subject and a key-authorization domain:
```java
TlsSni02Challenge challenge = auth.findChallenge(TlsSni02Challenge.TYPE);
String subject = challenge.getSubject(); // SAN-A
String sanB = challenge.getSanB(); // SAN-B
```
`subject` and `sanB` are basically domain names formed like in this example:
```
5bf0b9908ed73bc53ed3327afa52f76b.0a4bea00520f0753f42abe0bb39e3ea8.token.acme.invalid
14e2350a04434f93c2e0b6012968d99d.ed459b6a7a019d9695609b8514f9d63d.ka.acme.invalid
```
You need to create a self-signed certificate with both `subject` and `sanB` set as _Subject Alternative Name_. After that, configure your web server so it will use this certificate on a SNI request to `subject`.
The `TlsSni02Challenge` class does not generate a self-signed certificate, as it would require _Bouncy Castle_. However, there is a utility method in the _acme4j-utils_ module for this use case:
```java
KeyPair sniKeyPair = KeyPairUtils.createKeyPair(2048);
X509Certificate cert = CertificateUtils.createTlsSni02Certificate(sniKeyPair, subject, sanB);
```
Now use `cert` and `sniKeyPair` to let your web server respond to SNI requests to `subject`. The CA is not allowed to reveal `sanB`, so it will not perform SNI requests to that domain.
The challenge is completed when the CA was able to send the SNI request and get the correct certificate in return.
> __Note:__ The request is sent to port 443 only. If your domain has multiple IP addresses, the CA randomly selects one of them. There is no way to choose a different port or a fixed IP address.
This shell command line may be helpful to test your web server configuration:
```shell
echo QUIT | \
openssl s_client -servername $subject -connect $server_ip:443 | \
openssl x509 -text -noout
```
It should return a certificate with both `subject` and `sanB` set as `X509v3 Subject Alternative Name`.

View File

@ -14,7 +14,6 @@ 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.
* 53 (UDP): A simple DNS server for resolving test domain names, and providing `TXT` records for `dns-01` challenges.
* 5001: A simple HTTP server that responses with certificates for `tls-sni-02` challenges.
* 5002: A simple HTTP server that responses 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.
@ -33,4 +32,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`.
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.
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.

View File

@ -40,7 +40,6 @@
<item name="Challenges" href="challenge/index.html">
<item name="http-01" href="challenge/http-01.html"/>
<item name="dns-01" href="challenge/dns-01.html"/>
<item name="tls-sni-02" href="challenge/tls-sni-02.html"/>
</item>
<item name="CAs" href="ca/index.html">
<item name="Let's Encrypt" href="ca/letsencrypt.html"/>