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.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.TlsSni02Challenge;
import org.shredzone.acme4j.challenge.TokenChallenge; import org.shredzone.acme4j.challenge.TokenChallenge;
import org.shredzone.acme4j.connector.Connection; import org.shredzone.acme4j.connector.Connection;
import org.shredzone.acme4j.connector.DefaultConnection; 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<String, BiFunction<Session, JSON, Challenge>> map = new HashMap<>();
map.put(Dns01Challenge.TYPE, Dns01Challenge::new); map.put(Dns01Challenge.TYPE, Dns01Challenge::new);
map.put(TlsSni02Challenge.TYPE, TlsSni02Challenge::new);
map.put(Http01Challenge.TYPE, Http01Challenge::new); map.put(Http01Challenge.TYPE, Http01Challenge::new);
return Collections.unmodifiableMap(map); 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.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.TlsSni02Challenge;
import org.shredzone.acme4j.exception.AcmeException; import org.shredzone.acme4j.exception.AcmeException;
import org.shredzone.acme4j.exception.AcmeProtocolException; import org.shredzone.acme4j.exception.AcmeProtocolException;
import org.shredzone.acme4j.exception.AcmeRetryAfterException; import org.shredzone.acme4j.exception.AcmeRetryAfterException;
@ -67,11 +66,6 @@ public class AuthorizationTest {
Challenge c3 = authorization.findChallenge(Dns01Challenge.TYPE); Challenge c3 = authorization.findChallenge(Dns01Challenge.TYPE);
assertThat(c3, is(notNullValue())); assertThat(c3, is(notNullValue()));
assertThat(c3, is(instanceOf(Dns01Challenge.class))); 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(Http01Challenge.TYPE, Http01Challenge::new);
provider.putTestChallenge(Dns01Challenge.TYPE, Dns01Challenge::new); provider.putTestChallenge(Dns01Challenge.TYPE, Dns01Challenge::new);
provider.putTestChallenge(TlsSni02Challenge.TYPE, TlsSni02Challenge::new);
provider.putTestChallenge(DUPLICATE_TYPE, Challenge::new); provider.putTestChallenge(DUPLICATE_TYPE, Challenge::new);
Authorization authorization = new Authorization(session, locationUrl); 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.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.TlsSni02Challenge;
import org.shredzone.acme4j.challenge.TokenChallenge; import org.shredzone.acme4j.challenge.TokenChallenge;
import org.shredzone.acme4j.connector.Connection; import org.shredzone.acme4j.connector.Connection;
import org.shredzone.acme4j.connector.DefaultConnection; import org.shredzone.acme4j.connector.DefaultConnection;
@ -147,10 +146,6 @@ public class AbstractAcmeProviderTest {
assertThat(c3, not(nullValue())); assertThat(c3, not(nullValue()));
assertThat(c3, instanceOf(Dns01Challenge.class)); 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() JSON json6 = new JSONBuilder()
.put("type", "foobar-01") .put("type", "foobar-01")
.put("url", "https://example.com/some/challenge") .put("url", "https://example.com/some/challenge")

View File

@ -10,11 +10,6 @@
"url": "https://example.com/authz/asdf/1", "url": "https://example.com/authz/asdf/1",
"token": "DGyRejmCefe7v4NfDGDKfA" "token": "DGyRejmCefe7v4NfDGDKfA"
}, },
{
"type": "tls-sni-02",
"url": "https://example.com/authz/asdf/2",
"token": "VNLBdSiZ3LppU2CRG8bilqlwq4DuApJMg3ZJowU6JhQ"
},
{ {
"type": "duplicate-01", "type": "duplicate-01",
"url": "https://example.com/authz/asdf/3" "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.net.URI;
import java.security.KeyPair; import java.security.KeyPair;
import java.security.Security; import java.security.Security;
import java.security.cert.X509Certificate;
import java.util.Arrays; import java.util.Arrays;
import java.util.Collection; import java.util.Collection;
import javax.swing.JOptionPane; import javax.swing.JOptionPane;
import org.bouncycastle.jce.provider.BouncyCastleProvider; import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.bouncycastle.openssl.jcajce.JcaPEMWriter;
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.TlsSni02Challenge;
import org.shredzone.acme4j.exception.AcmeException; import org.shredzone.acme4j.exception.AcmeException;
import org.shredzone.acme4j.util.CSRBuilder; import org.shredzone.acme4j.util.CSRBuilder;
import org.shredzone.acme4j.util.CertificateUtils;
import org.shredzone.acme4j.util.KeyPairUtils; import org.shredzone.acme4j.util.KeyPairUtils;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
@ -66,7 +62,7 @@ public class ClientTest {
private static final Logger LOG = LoggerFactory.getLogger(ClientTest.class); 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 * Generates a certificate for the given domains. Also takes care for the registration
@ -218,10 +214,6 @@ public class ClientTest {
case DNS: case DNS:
challenge = dnsChallenge(auth); challenge = dnsChallenge(auth);
break; break;
case TLSSNI:
challenge = tlsSniChallenge(auth);
break;
} }
if (challenge == null) { if (challenge == null) {
@ -334,62 +326,6 @@ public class ClientTest {
return challenge; 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 * Presents the instructions for preparing the challenge validation, and waits for
* dismissal. If the user cancelled the dialog, an exception is thrown. * dismissal. If the user cancelled the dialog, an exception is thrown.

View File

@ -178,7 +178,6 @@
<from>openjdk:8-jre</from> <from>openjdk:8-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>

View File

@ -19,7 +19,6 @@ import java.util.ResourceBundle;
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.TlsSniServer;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
@ -27,8 +26,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, TLS-SNI server, and DNS * A mock server to test Pebble. It provides a HTTP server and DNS server. The servers can
* server. The servers can be configured remotely via simple HTTP POST requests. * 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!
@ -41,22 +40,18 @@ 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 tlsSniPort;
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 TlsSniServer tlsSniServer;
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"));
tlsSniPort = Integer.parseInt(bundle.getString("tlsSni.port"));
dnsServer = new DnsServer(); dnsServer = new DnsServer();
httpServer = new HttpServer(); httpServer = new HttpServer();
tlsSniServer = new TlsSniServer();
appServer = new AppServer(appPort); appServer = new AppServer(appPort);
} }
@ -83,20 +78,12 @@ public class BammBamm {
return httpServer; return httpServer;
} }
/**
* Returns the {@link TlsSniServer} instance.
*/
public TlsSniServer getTlsSniServer() {
return tlsSniServer;
}
/** /**
* Starts the servers. * Starts the servers.
*/ */
public void start() { public void start() {
dnsServer.start(dnsPort); dnsServer.start(dnsPort);
httpServer.start(httpPort); httpServer.start(httpPort);
tlsSniServer.start(tlsSniPort);
try { try {
appServer.start(NanoHTTPD.SOCKET_READ_TIMEOUT, true); appServer.start(NanoHTTPD.SOCKET_READ_TIMEOUT, true);
@ -112,7 +99,6 @@ public class BammBamm {
*/ */
public void stop() { public void stop() {
appServer.stop(); appServer.stop();
tlsSniServer.stop();
httpServer.stop(); httpServer.stop();
dnsServer.stop(); dnsServer.stop();
@ -134,9 +120,6 @@ 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(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.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 org.apache.http.HttpResponse; import org.apache.http.HttpResponse;
@ -136,41 +131,6 @@ public class BammBammClient {
.submit(); .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. * Creates a new {@link Request} object.
* *
@ -187,7 +147,6 @@ public class BammBammClient {
*/ */
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<>();
@ -239,19 +198,6 @@ 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.
*/ */

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 server port
http.port = 5002 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 `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! 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.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.TlsSni02Challenge;
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.
@ -47,32 +45,6 @@ public class OrderIT extends PebbleITBase {
private static final String TEST_DOMAIN = "example.com"; 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. * Test if a certificate can be ordered via http-01 challenge.
*/ */

View File

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

View File

@ -13,29 +13,12 @@
*/ */
package org.shredzone.acme4j.util; package org.shredzone.acme4j.util;
import java.io.ByteArrayInputStream;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.io.InputStreamReader; 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.openssl.PEMParser;
import org.bouncycastle.operator.OperatorCreationException;
import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder;
import org.bouncycastle.pkcs.PKCS10CertificationRequest; import org.bouncycastle.pkcs.PKCS10CertificationRequest;
import org.shredzone.acme4j.challenge.TlsSni02Challenge;
/** /**
* Utility class offering convenience methods for certificates. * 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.Constructor;
import java.lang.reflect.Modifier; import java.lang.reflect.Modifier;
import java.security.KeyPair; 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.bouncycastle.pkcs.PKCS10CertificationRequest;
import org.junit.Test; import org.junit.Test;
@ -41,30 +32,6 @@ import org.junit.Test;
*/ */
public class CertificateUtilsTest { 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. * Test if {@link CertificateUtils#readCSR(InputStream)} reads an identical CSR.
*/ */
@ -100,23 +67,4 @@ public class CertificateUtilsTest {
constructor.newInstance(); 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) * [http-01](./http-01.html)
* [dns-01](./dns-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. * 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.
* 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. * 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. 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`. 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="Challenges" href="challenge/index.html">
<item name="http-01" href="challenge/http-01.html"/> <item name="http-01" href="challenge/http-01.html"/>
<item name="dns-01" href="challenge/dns-01.html"/> <item name="dns-01" href="challenge/dns-01.html"/>
<item name="tls-sni-02" href="challenge/tls-sni-02.html"/>
</item> </item>
<item name="CAs" href="ca/index.html"> <item name="CAs" href="ca/index.html">
<item name="Let's Encrypt" href="ca/letsencrypt.html"/> <item name="Let's Encrypt" href="ca/letsencrypt.html"/>