mirror of https://github.com/shred/acme4j
Remove tls-sni-02 challenge
parent
472f1497db
commit
137c2c7dd0
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
|
||||
}
|
|
@ -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")
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -1,6 +0,0 @@
|
|||
{
|
||||
"type": "tls-sni-02",
|
||||
"url": "https://example.com/acme/authz/0",
|
||||
"status": "pending",
|
||||
"token": "VNLBdSiZ3LppU2CRG8bilqlwq4DuApJMg3ZJowU6JhQ"
|
||||
}
|
|
@ -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.
|
||||
|
|
|
@ -178,7 +178,6 @@
|
|||
<from>openjdk:8-jre</from>
|
||||
<ports>
|
||||
<port>53/udp</port>
|
||||
<port>5001</port>
|
||||
<port>5002</port>
|
||||
<port>14001</port>
|
||||
</ports>
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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.
|
||||
*/
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -7,6 +7,3 @@ dns.port = 53
|
|||
|
||||
# HTTP server port
|
||||
http.port = 5002
|
||||
|
||||
# TLS-SNI server port
|
||||
tlsSni.port = 5001
|
||||
|
|
|
@ -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!
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -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.
|
||||
*/
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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`.
|
|
@ -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.
|
||||
|
|
|
@ -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"/>
|
||||
|
|
Loading…
Reference in New Issue