mirror of https://github.com/shred/acme4j
Add support for tls-sni-02
parent
acd6f8019d
commit
8deceb473c
|
@ -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();
|
||||||
|
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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;
|
||||||
|
|
|
@ -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)));
|
||||||
|
|
||||||
|
|
|
@ -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";
|
|
@ -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());
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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);
|
||||||
|
|
|
@ -163,4 +163,11 @@ tlsSniChallenge = \
|
||||||
"token": "VNLBdSiZ3LppU2CRG8bilqlwq4DuApJMg3ZJowU6JhQ" \
|
"token": "VNLBdSiZ3LppU2CRG8bilqlwq4DuApJMg3ZJowU6JhQ" \
|
||||||
}
|
}
|
||||||
|
|
||||||
|
tlsSni02Challenge = \
|
||||||
|
{ \
|
||||||
|
"type":"tls-sni-02", \
|
||||||
|
"status":"pending", \
|
||||||
|
"token": "VNLBdSiZ3LppU2CRG8bilqlwq4DuApJMg3ZJowU6JhQ" \
|
||||||
|
}
|
||||||
|
|
||||||
#
|
#
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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.
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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`.
|
|
@ -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">
|
||||||
|
|
Loading…
Reference in New Issue