Add support for tls-sni-02

pull/17/merge
Richard Körber 2016-03-19 15:42:22 +01:00
parent acd6f8019d
commit 8deceb473c
15 changed files with 341 additions and 22 deletions

View File

@ -23,8 +23,12 @@ import org.shredzone.acme4j.exception.AcmeProtocolException;
/** /**
* Implements the {@value TYPE} challenge. * Implements the {@value TYPE} challenge.
* *
* @deprecated Use {@link TlsSni02Challenge} if supported by the CA. This challenge will
* be removed as soon as Let's Encrypt removes support for
* {@link TlsSni01Challenge}.
* @author Richard "Shred" Körber * @author Richard "Shred" Körber
*/ */
@Deprecated
public class TlsSni01Challenge extends GenericTokenChallenge { public class TlsSni01Challenge extends GenericTokenChallenge {
private static final long serialVersionUID = 7370329525205430573L; private static final long serialVersionUID = 7370329525205430573L;
private static final char[] HEX = "0123456789abcdef".toCharArray(); private static final char[] HEX = "0123456789abcdef".toCharArray();

View File

@ -0,0 +1,98 @@
/*
* 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 java.io.UnsupportedEncodingException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import org.shredzone.acme4j.Registration;
import org.shredzone.acme4j.exception.AcmeProtocolException;
/**
* Implements the {@value TYPE} challenge.
*
* @author Richard "Shred" Körber
*/
public class TlsSni02Challenge extends GenericTokenChallenge {
private static final long serialVersionUID = 8921833167878544518L;
private static final char[] HEX = "0123456789abcdef".toCharArray();
/**
* Challenge type name: {@value}
*/
public static final String TYPE = "tls-sni-02";
private String subject;
private String sanB;
/**
* 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() {
assertIsAuthorized();
return subject;
}
/**
* Returns the key authorization, which is to be used as "SAN-B" in a self-signed
* certificate.
*/
public String getSanB() {
assertIsAuthorized();
return sanB;
}
@Override
public void authorize(Registration registration) {
super.authorize(registration);
String tokenHash = computeHash(getToken());
subject = tokenHash.substring(0, 32) + '.' + tokenHash.substring(32) + ".token.acme.invalid";
String kaHash = computeHash(getAuthorization());
sanB = kaHash.substring(0, 32) + '.' + kaHash.substring(32) + ".ka.acme.invalid";
}
@Override
protected boolean acceptable(String type) {
return TYPE.equals(type);
}
/**
* Computes a hash according to the specifications.
*
* @param z
* Value to be hashed
* @return Hash
*/
private String computeHash(String z) {
try {
MessageDigest md = MessageDigest.getInstance("SHA-256");
md.update(z.getBytes("UTF-8"));
byte[] raw = md.digest();
char[] result = new char[raw.length * 2];
for (int ix = 0; ix < raw.length; ix++) {
int val = raw[ix] & 0xFF;
result[ix * 2] = HEX[val >>> 4];
result[ix * 2 + 1] = HEX[val & 0x0F];
}
return new String(result);
} catch (NoSuchAlgorithmException | UnsupportedEncodingException ex) {
throw new AcmeProtocolException("Could not compute hash", ex);
}
}
}

View File

@ -21,6 +21,7 @@ import org.shredzone.acme4j.challenge.Dns01Challenge;
import org.shredzone.acme4j.challenge.Http01Challenge; import org.shredzone.acme4j.challenge.Http01Challenge;
import org.shredzone.acme4j.challenge.ProofOfPossession01Challenge; import org.shredzone.acme4j.challenge.ProofOfPossession01Challenge;
import org.shredzone.acme4j.challenge.TlsSni01Challenge; import org.shredzone.acme4j.challenge.TlsSni01Challenge;
import org.shredzone.acme4j.challenge.TlsSni02Challenge;
import org.shredzone.acme4j.connector.Connection; import org.shredzone.acme4j.connector.Connection;
import org.shredzone.acme4j.connector.HttpConnector; import org.shredzone.acme4j.connector.HttpConnector;
import org.shredzone.acme4j.impl.DefaultConnection; import org.shredzone.acme4j.impl.DefaultConnection;
@ -35,6 +36,7 @@ import org.shredzone.acme4j.impl.GenericAcmeClient;
* *
* @author Richard "Shred" Körber * @author Richard "Shred" Körber
*/ */
@SuppressWarnings("deprecation") // must also provide deprecated challenges
public abstract class AbstractAcmeClientProvider implements AcmeClientProvider { public abstract class AbstractAcmeClientProvider implements AcmeClientProvider {
/** /**
@ -70,6 +72,7 @@ public abstract class AbstractAcmeClientProvider implements AcmeClientProvider {
switch (type) { switch (type) {
case Dns01Challenge.TYPE: return new Dns01Challenge(); case Dns01Challenge.TYPE: return new Dns01Challenge();
case TlsSni01Challenge.TYPE: return new TlsSni01Challenge(); case TlsSni01Challenge.TYPE: return new TlsSni01Challenge();
case TlsSni02Challenge.TYPE: return new TlsSni02Challenge();
case ProofOfPossession01Challenge.TYPE: return new ProofOfPossession01Challenge(); case ProofOfPossession01Challenge.TYPE: return new ProofOfPossession01Challenge();
case Http01Challenge.TYPE: return new Http01Challenge(); case Http01Challenge.TYPE: return new Http01Challenge();
default: return null; default: return null;

View File

@ -33,7 +33,7 @@ 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.ProofOfPossession01Challenge; import org.shredzone.acme4j.challenge.ProofOfPossession01Challenge;
import org.shredzone.acme4j.challenge.TlsSni01Challenge; import org.shredzone.acme4j.challenge.TlsSni02Challenge;
/** /**
* Unit tests for {@link Authorization}. * Unit tests for {@link Authorization}.
@ -51,7 +51,7 @@ public class AuthorizationTest {
public void setup() { public void setup() {
Challenge challenge1 = setupChallenge(Http01Challenge.TYPE, new Http01Challenge()); Challenge challenge1 = setupChallenge(Http01Challenge.TYPE, new Http01Challenge());
Challenge challenge2 = setupChallenge(Dns01Challenge.TYPE, new Dns01Challenge()); Challenge challenge2 = setupChallenge(Dns01Challenge.TYPE, new Dns01Challenge());
Challenge challenge3 = setupChallenge(TlsSni01Challenge.TYPE, new TlsSni01Challenge()); Challenge challenge3 = setupChallenge(TlsSni02Challenge.TYPE, new TlsSni02Challenge());
List<Challenge> challenges = new ArrayList<>(); List<Challenge> challenges = new ArrayList<>();
challenges.add(challenge1); challenges.add(challenge1);
@ -111,7 +111,7 @@ public class AuthorizationTest {
assertThat(c2, is(instanceOf(Http01Challenge.class))); assertThat(c2, is(instanceOf(Http01Challenge.class)));
// TlsSniChallenge is available, but not as standalone challenge // TlsSniChallenge is available, but not as standalone challenge
Challenge c3 = authorization.findChallenge(TlsSni01Challenge.TYPE); Challenge c3 = authorization.findChallenge(TlsSni02Challenge.TYPE);
assertThat(c3, is(nullValue())); assertThat(c3, is(nullValue()));
} }
@ -128,25 +128,25 @@ public class AuthorizationTest {
assertThat(c1, contains(instanceOf(Http01Challenge.class))); assertThat(c1, contains(instanceOf(Http01Challenge.class)));
// Available combined challenge // Available combined challenge
Collection<Challenge> c2 = authorization.findCombination(Dns01Challenge.TYPE, TlsSni01Challenge.TYPE); Collection<Challenge> c2 = authorization.findCombination(Dns01Challenge.TYPE, TlsSni02Challenge.TYPE);
assertThat(c2, hasSize(2)); assertThat(c2, hasSize(2));
assertThat(c2, contains(instanceOf(Dns01Challenge.class), assertThat(c2, contains(instanceOf(Dns01Challenge.class),
instanceOf(TlsSni01Challenge.class))); instanceOf(TlsSni02Challenge.class)));
// Order does not matter // Order does not matter
Collection<Challenge> c3 = authorization.findCombination(TlsSni01Challenge.TYPE, Dns01Challenge.TYPE); Collection<Challenge> c3 = authorization.findCombination(TlsSni02Challenge.TYPE, Dns01Challenge.TYPE);
assertThat(c3, hasSize(2)); assertThat(c3, hasSize(2));
assertThat(c3, contains(instanceOf(Dns01Challenge.class), assertThat(c3, contains(instanceOf(Dns01Challenge.class),
instanceOf(TlsSni01Challenge.class))); instanceOf(TlsSni02Challenge.class)));
// Finds smaller combinations as well // Finds smaller combinations as well
Collection<Challenge> c4 = authorization.findCombination(Dns01Challenge.TYPE, TlsSni01Challenge.TYPE, ProofOfPossession01Challenge.TYPE); Collection<Challenge> c4 = authorization.findCombination(Dns01Challenge.TYPE, TlsSni02Challenge.TYPE, ProofOfPossession01Challenge.TYPE);
assertThat(c4, hasSize(2)); assertThat(c4, hasSize(2));
assertThat(c4, contains(instanceOf(Dns01Challenge.class), assertThat(c4, contains(instanceOf(Dns01Challenge.class),
instanceOf(TlsSni01Challenge.class))); instanceOf(TlsSni02Challenge.class)));
// Finds the smallest possible combination // Finds the smallest possible combination
Collection<Challenge> c5 = authorization.findCombination(Dns01Challenge.TYPE, TlsSni01Challenge.TYPE, Http01Challenge.TYPE); Collection<Challenge> c5 = authorization.findCombination(Dns01Challenge.TYPE, TlsSni02Challenge.TYPE, Http01Challenge.TYPE);
assertThat(c5, hasSize(1)); assertThat(c5, hasSize(1));
assertThat(c5, contains(instanceOf(Http01Challenge.class))); assertThat(c5, contains(instanceOf(Http01Challenge.class)));

View File

@ -31,7 +31,8 @@ import org.shredzone.acme4j.util.TestUtils;
* *
* @author Richard "Shred" Körber * @author Richard "Shred" Körber
*/ */
public class TlsSniChallengeTest { @SuppressWarnings("deprecation") // must test a deprecated challenge
public class TlsSni01ChallengeTest {
private static final String KEY_AUTHORIZATION = private static final String KEY_AUTHORIZATION =
"VNLBdSiZ3LppU2CRG8bilqlwq4DuApJMg3ZJowU6JhQ.HnWjTDnyqlCrm6tZ-6wX-TrEXgRdeNu9G71gqxSO6o0"; "VNLBdSiZ3LppU2CRG8bilqlwq4DuApJMg3ZJowU6JhQ.HnWjTDnyqlCrm6tZ-6wX-TrEXgRdeNu9G71gqxSO6o0";

View File

@ -0,0 +1,79 @@
/*
* 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.*;
import static uk.co.datumedge.hamcrest.json.SameJSONAs.sameJSONAs;
import java.io.IOException;
import java.security.KeyPair;
import org.junit.Test;
import org.shredzone.acme4j.Registration;
import org.shredzone.acme4j.Status;
import org.shredzone.acme4j.util.ClaimBuilder;
import org.shredzone.acme4j.util.TestUtils;
/**
* Unit tests for {@link TlsSni02Challenge}.
*
* @author Richard "Shred" Körber
*/
public class TlsSni02ChallengeTest {
private static final String KEY_AUTHORIZATION =
"VNLBdSiZ3LppU2CRG8bilqlwq4DuApJMg3ZJowU6JhQ.HnWjTDnyqlCrm6tZ-6wX-TrEXgRdeNu9G71gqxSO6o0";
/**
* Test that {@link TlsSni02Challenge} generates a correct authorization key.
*/
@Test
public void testTlsSni02Challenge() throws IOException {
KeyPair keypair = TestUtils.createKeyPair();
Registration reg = new Registration(keypair);
TlsSni02Challenge challenge = new TlsSni02Challenge();
challenge.unmarshall(TestUtils.getJsonAsMap("tlsSni02Challenge"));
assertThat(challenge.getType(), is(TlsSni02Challenge.TYPE));
assertThat(challenge.getStatus(), is(Status.PENDING));
try {
challenge.getSubject();
fail("getSubject() without previous authorize()");
} catch (IllegalStateException ex) {
// expected
}
try {
challenge.getSanB();
fail("getSanB() without previous authorize()");
} catch (IllegalStateException ex) {
// expected
}
challenge.authorize(reg);
assertThat(challenge.getSubject(), is("5bf0b9908ed73bc53ed3327afa52f76b.0a4bea00520f0753f42abe0bb39e3ea8.token.acme.invalid"));
assertThat(challenge.getSanB(), is("14e2350a04434f93c2e0b6012968d99d.ed459b6a7a019d9695609b8514f9d63d.ka.acme.invalid"));
ClaimBuilder cb = new ClaimBuilder();
challenge.respond(cb);
assertThat(cb.toString(), sameJSONAs("{\"keyAuthorization\"=\""
+ KEY_AUTHORIZATION + "\"}").allowingExtraUnexpectedFields());
}
}

View File

@ -25,7 +25,7 @@ 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.ProofOfPossession01Challenge; import org.shredzone.acme4j.challenge.ProofOfPossession01Challenge;
import org.shredzone.acme4j.challenge.TlsSni01Challenge; import org.shredzone.acme4j.challenge.TlsSni02Challenge;
/** /**
* Unit tests for {@link AbstractAcmeClientProvider}. * Unit tests for {@link AbstractAcmeClientProvider}.
@ -76,6 +76,7 @@ public class AbstractAcmeClientProviderTest {
* Test that challenges are generated properly. * Test that challenges are generated properly.
*/ */
@Test @Test
@SuppressWarnings("deprecation") // must test deprecated challenges
public void testCreateChallenge() { public void testCreateChallenge() {
AbstractAcmeClientProvider provider = new AbstractAcmeClientProvider() { AbstractAcmeClientProvider provider = new AbstractAcmeClientProvider() {
@Override @Override
@ -104,12 +105,16 @@ public class AbstractAcmeClientProviderTest {
assertThat(c4, not(nullValue())); assertThat(c4, not(nullValue()));
assertThat(c4, instanceOf(ProofOfPossession01Challenge.class)); assertThat(c4, instanceOf(ProofOfPossession01Challenge.class));
Challenge c5 = provider.createChallenge(TlsSni01Challenge.TYPE); Challenge c5 = provider.createChallenge(org.shredzone.acme4j.challenge.TlsSni01Challenge.TYPE);
assertThat(c5, not(nullValue())); assertThat(c5, not(nullValue()));
assertThat(c5, instanceOf(TlsSni01Challenge.class)); assertThat(c5, instanceOf(org.shredzone.acme4j.challenge.TlsSni01Challenge.class));
Challenge c6 = provider.createChallenge("foobar-01"); Challenge c6 = provider.createChallenge(TlsSni02Challenge.TYPE);
assertThat(c6, is(nullValue())); assertThat(c6, not(nullValue()));
assertThat(c6, instanceOf(TlsSni02Challenge.class));
Challenge c7 = provider.createChallenge("foobar-01");
assertThat(c7, is(nullValue()));
try { try {
provider.createChallenge(null); provider.createChallenge(null);

View File

@ -163,4 +163,11 @@ tlsSniChallenge = \
"token": "VNLBdSiZ3LppU2CRG8bilqlwq4DuApJMg3ZJowU6JhQ" \ "token": "VNLBdSiZ3LppU2CRG8bilqlwq4DuApJMg3ZJowU6JhQ" \
} }
tlsSni02Challenge = \
{ \
"type":"tls-sni-02", \
"status":"pending", \
"token": "VNLBdSiZ3LppU2CRG8bilqlwq4DuApJMg3ZJowU6JhQ" \
}
# #

View File

@ -31,7 +31,6 @@ import org.bouncycastle.jce.provider.BouncyCastleProvider;
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.TlsSni01Challenge;
import org.shredzone.acme4j.exception.AcmeConflictException; import org.shredzone.acme4j.exception.AcmeConflictException;
import org.shredzone.acme4j.exception.AcmeException; import org.shredzone.acme4j.exception.AcmeException;
import org.shredzone.acme4j.exception.AcmeUnauthorizedException; import org.shredzone.acme4j.exception.AcmeUnauthorizedException;
@ -267,11 +266,12 @@ public class ClientTest {
/** /**
* Prepares TLS-SNI challenge. * Prepares TLS-SNI challenge.
*/ */
@SuppressWarnings("deprecation") // until tls-sni-02 is supported
public Challenge tlsSniChallenge(Authorization auth, Registration reg, String domain) throws AcmeException { public Challenge tlsSniChallenge(Authorization auth, Registration reg, String domain) throws AcmeException {
// Find a single tls-sni-01 challenge // Find a single tls-sni-01 challenge
TlsSni01Challenge challenge = auth.findChallenge(TlsSni01Challenge.TYPE); org.shredzone.acme4j.challenge.TlsSni01Challenge challenge = auth.findChallenge(org.shredzone.acme4j.challenge.TlsSni01Challenge.TYPE);
if (challenge == null) { if (challenge == null) {
LOG.error("Found no " + TlsSni01Challenge.TYPE + " challenge, don't know what to do..."); LOG.error("Found no " + org.shredzone.acme4j.challenge.TlsSni01Challenge.TYPE + " challenge, don't know what to do...");
return null; return null;
} }

View File

@ -37,7 +37,7 @@ import org.bouncycastle.openssl.jcajce.JcaPEMWriter;
import org.bouncycastle.operator.OperatorCreationException; import org.bouncycastle.operator.OperatorCreationException;
import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder; import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder;
import org.bouncycastle.pkcs.PKCS10CertificationRequest; import org.bouncycastle.pkcs.PKCS10CertificationRequest;
import org.shredzone.acme4j.challenge.TlsSni01Challenge; import org.shredzone.acme4j.challenge.TlsSni02Challenge;
/** /**
* Utility class offering convenience methods for certificates. * Utility class offering convenience methods for certificates.
@ -113,14 +113,18 @@ public final class CertificateUtils {
/** /**
* Creates a self-signed {@link X509Certificate} that can be used for * Creates a self-signed {@link X509Certificate} that can be used for
* {@link TlsSni01Challenge}. The certificate is valid for 7 days. * {@link org.shredzone.acme4j.challenge.TlsSni01Challenge}. The certificate is valid
* for 7 days.
* *
* @param keypair * @param keypair
* A domain {@link KeyPair} to be used for the challenge * A domain {@link KeyPair} to be used for the challenge
* @param subject * @param subject
* Subject to create a certificate for * Subject to create a certificate for
* @return Created certificate * @return Created certificate
* @deprecated Will be removed when
* {@link org.shredzone.acme4j.challenge.TlsSni01Challenge} is removed
*/ */
@Deprecated
public static X509Certificate createTlsSniCertificate(KeyPair keypair, String subject) throws IOException { public static X509Certificate createTlsSniCertificate(KeyPair keypair, String subject) throws IOException {
final long now = System.currentTimeMillis(); final long now = System.currentTimeMillis();
final long validSpanMs = 7 * 24 * 60 * 60 * 1000L; final long validSpanMs = 7 * 24 * 60 * 60 * 1000L;
@ -151,4 +155,48 @@ 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 {
final long now = System.currentTimeMillis();
final long validSpanMs = 7 * 24 * 60 * 60 * 1000L;
final String signatureAlg = "SHA256withRSA";
try {
X500Name issuer = new X500Name("CN=acme.invalid");
BigInteger serial = BigInteger.valueOf(now);
Date notBefore = new Date(now);
Date notAfter = new Date(now + validSpanMs);
JcaX509v3CertificateBuilder certBuilder = new JcaX509v3CertificateBuilder(
issuer, serial, notBefore, notAfter, issuer, keypair.getPublic());
GeneralName[] gns = new GeneralName[2];
gns[0] = new GeneralName(GeneralName.dNSName, sanA);
gns[1] = new GeneralName(GeneralName.dNSName, sanB);
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

@ -90,6 +90,7 @@ public class CertificateUtilsTest {
* good certificate. * good certificate.
*/ */
@Test @Test
@SuppressWarnings("deprecation") // test deprecated method
public void testCreateTlsSniCertificate() throws IOException, CertificateParsingException { public void testCreateTlsSniCertificate() throws IOException, CertificateParsingException {
String subject = "30c452b9bd088cdbc2c4094947025d7c.7364ea602ac325a1b55ceaae024fbe29.acme.invalid"; String subject = "30c452b9bd088cdbc2c4094947025d7c.7364ea602ac325a1b55ceaae024fbe29.acme.invalid";
@ -108,6 +109,30 @@ public class CertificateUtilsTest {
assertThat(getSANs(cert), containsInAnyOrder(subject)); assertThat(getSANs(cert), containsInAnyOrder(subject));
} }
/**
* Test if {@link CertificateUtils#createTlsSni02Certificate(KeyPair, 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);
Date now = new Date();
Date end = new Date(now.getTime() + (8 * 24 * 60 * 60 * 1000L));
assertThat(cert, not(nullValue()));
assertThat(cert.getNotAfter(), is(greaterThan(now)));
assertThat(cert.getNotAfter(), is(lessThan(end)));
assertThat(cert.getNotBefore(), is(lessThanOrEqualTo(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.
*/ */

View File

@ -6,9 +6,10 @@ There are different kind of challenges. The most simple is maybe the HTTP challe
The CA offers one or more sets of challenges. At least one set has to be completed in order to prove ownership. The CA offers one or more sets of challenges. At least one set has to be completed in order to prove ownership.
The ACME specifications define four standard challenges: 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-01](./tls-sni-01.html) * [tls-sni-01](./tls-sni-01.html)
* [tls-sni-02](./tls-sni-02.html)
* [proof-of-possession-01](./proof-of-possession-01.html) * [proof-of-possession-01](./proof-of-possession-01.html)

View File

@ -1,5 +1,7 @@
# tls-sni-01 Challenge # tls-sni-01 Challenge
> **DEPRECATED:** According to the ACME specifications, this challenge will be replaced by [tls-sni-02](./tls-sni-02.html). However, _Let's Encrypt_ does not currently support `tls-sni-02`. For the time being, _acme4j_ supports both challenges. To be on the safe side, request both challenges and process the one that is returned.
With the `tls-sni-01` 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. With the `tls-sni-01` 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.
After authorizing the challenge, `TlsSni01Challenge` provides a subject: After authorizing the challenge, `TlsSni01Challenge` provides a subject:

View File

@ -0,0 +1,45 @@
# tls-sni-02 Challenge
> **NOTE:** According to the ACME specifications, this challenge will replace [tls-sni-01](./tls-sni-01.html). However, _Let's Encrypt_ does not currently support `tls-sni-02`. For the time being, _acme4j_ supports both challenges. To be on the safe side, request both challenges and process the one that is returned.
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.
After authorizing the challenge, `TlsSni02Challenge` provides a subject and a key-authorization domain:
```java
TlsSni02Challenge challenge = auth.findChallenge(TlsSni02Challenge.TYPE);
challenge.authorize(registration);
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.
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

@ -40,6 +40,7 @@
<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-01" href="challenge/tls-sni-01.html"/> <item name="tls-sni-01" href="challenge/tls-sni-01.html"/>
<item name="tls-sni-02" href="challenge/tls-sni-02.html"/>
<item name="proof-of-possession-01" href="challenge/proof-of-possession-01.html"/> <item name="proof-of-possession-01" href="challenge/proof-of-possession-01.html"/>
</item> </item>
<item name="CAs" href="ca/index.html"> <item name="CAs" href="ca/index.html">