mirror of https://github.com/shred/acme4j
Add support for draft-ietf-acme-dns-account-label
parent
1ed293c5bb
commit
c0d96e709e
|
@ -17,6 +17,7 @@ This Java client helps to connect to an ACME server, and performing all necessar
|
|||
* Supports [RFC 9444](https://tools.ietf.org/html/rfc9444) for subdomain validation
|
||||
* Supports [draft-ietf-acme-ari-07](https://www.ietf.org/archive/id/draft-ietf-acme-ari-07.html) for renewal information (experimental)
|
||||
* Supports [draft-aaron-acme-profiles-00](https://www.ietf.org/archive/id/draft-aaron-acme-profiles-00.html) for certificate profiles (experimental)
|
||||
* Supports [draft-ietf-acme-dns-account-label-00](https://datatracker.ietf.org/doc/draft-ietf-acme-dns-account-label/) for DNS labeled with ACME account ID challenges (experimental)
|
||||
* Easy to use Java API
|
||||
* Requires JRE 17 or higher
|
||||
* Supports [Buypass](https://buypass.com/), [Google Trust Services](https://pki.goog/), [Let's Encrypt](https://letsencrypt.org/), [SSL.com](https://www.ssl.com/), [ZeroSSL](https://zerossl.com/), and all other CAs that comply with the ACME protocol (RFC 8555). Note that _acme4j_ is an independent project that is not supported or endorsed by any of the CAs.
|
||||
|
|
|
@ -49,7 +49,9 @@ public class Dns01Challenge extends TokenChallenge {
|
|||
* @return Resource Record name (e.g. {@code _acme-challenge.www.example.org.}, note
|
||||
* the trailing full stop character).
|
||||
* @since 2.14
|
||||
* @deprecated Use {@link #getRRName(Identifier)}
|
||||
*/
|
||||
@Deprecated
|
||||
public static String toRRName(Identifier identifier) {
|
||||
return toRRName(identifier.getDomain());
|
||||
}
|
||||
|
@ -63,7 +65,9 @@ public class Dns01Challenge extends TokenChallenge {
|
|||
* @return Resource Record name (e.g. {@code _acme-challenge.www.example.org.}, note
|
||||
* the trailing full stop character).
|
||||
* @since 2.14
|
||||
* @deprecated Use {@link #getRRName(String)}
|
||||
*/
|
||||
@Deprecated
|
||||
public static String toRRName(String domain) {
|
||||
return RECORD_NAME_PREFIX + '.' + domain + '.';
|
||||
}
|
||||
|
@ -80,6 +84,34 @@ public class Dns01Challenge extends TokenChallenge {
|
|||
super(login, data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a domain identifier to the Resource Record name to be used for the DNS TXT
|
||||
* record.
|
||||
*
|
||||
* @param identifier
|
||||
* Domain {@link Identifier} of the domain to be validated
|
||||
* @return Resource Record name (e.g. {@code _acme-challenge.www.example.org.}, note
|
||||
* the trailing full stop character).
|
||||
* @since 3.6.0
|
||||
*/
|
||||
public String getRRName(Identifier identifier) {
|
||||
return toRRName(identifier);
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a domain identifier to the Resource Record name to be used for the DNS TXT
|
||||
* record.
|
||||
*
|
||||
* @param domain
|
||||
* Domain name to be validated
|
||||
* @return Resource Record name (e.g. {@code _acme-challenge.www.example.org.}, note
|
||||
* the trailing full stop character).
|
||||
* @since 3.6.0
|
||||
*/
|
||||
public String getRRName(String domain) {
|
||||
return toRRName(domain);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the digest string to be set in the domain's {@code _acme-challenge} TXT
|
||||
* record.
|
||||
|
|
|
@ -0,0 +1,106 @@
|
|||
/*
|
||||
* acme4j - Java ACME client
|
||||
*
|
||||
* Copyright (C) 2025 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 java.io.Serial;
|
||||
import java.net.URL;
|
||||
import java.util.Arrays;
|
||||
import java.util.Locale;
|
||||
|
||||
import org.shredzone.acme4j.Identifier;
|
||||
import org.shredzone.acme4j.Login;
|
||||
import org.shredzone.acme4j.toolbox.JSON;
|
||||
|
||||
/**
|
||||
* Implements the {@value TYPE} challenge. It requires a specific DNS record for domain
|
||||
* validation. See the acme4j documentation for a detailed explanation.
|
||||
*
|
||||
* @draft This class is currently based on an RFC draft. It may be changed or removed
|
||||
* without notice to reflect future changes to the draft. SemVer rules do not apply here.
|
||||
* @since 3.6.0
|
||||
*/
|
||||
public class DnsAccount01Challenge extends TokenChallenge {
|
||||
@Serial
|
||||
private static final long serialVersionUID = -1098129409378900733L;
|
||||
|
||||
/**
|
||||
* Challenge type name: {@value}
|
||||
*/
|
||||
public static final String TYPE = "dns-account-01";
|
||||
|
||||
/**
|
||||
* Creates a new generic {@link DnsAccount01Challenge} object.
|
||||
*
|
||||
* @param login
|
||||
* {@link Login} the resource is bound with
|
||||
* @param data
|
||||
* {@link JSON} challenge data
|
||||
*/
|
||||
public DnsAccount01Challenge(Login login, JSON data) {
|
||||
super(login, data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a domain identifier to the Resource Record name to be used for the DNS TXT
|
||||
* record.
|
||||
*
|
||||
* @param identifier
|
||||
* {@link Identifier} to be validated
|
||||
* @return Resource Record name (e.g.
|
||||
* {@code _ujmmovf2vn55tgye._acme-challenge.example.org.}, note the trailing full stop
|
||||
* character).
|
||||
*/
|
||||
public String getRRName(Identifier identifier) {
|
||||
return getRRName(identifier.getDomain());
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a domain identifier to the Resource Record name to be used for the DNS TXT
|
||||
* record.
|
||||
*
|
||||
* @param domain
|
||||
* Domain name to be validated
|
||||
* @return Resource Record name (e.g.
|
||||
* {@code _ujmmovf2vn55tgye._acme-challenge.example.org.}, note the trailing full stop
|
||||
* character).
|
||||
*/
|
||||
public String getRRName(String domain) {
|
||||
return getPrefix(getLogin().getAccount().getLocation()) + '.' + domain + '.';
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the digest string to be set in the domain's TXT record.
|
||||
*/
|
||||
public String getDigest() {
|
||||
return base64UrlEncode(sha256hash(getAuthorization()));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the prefix of an account location.
|
||||
*/
|
||||
private String getPrefix(URL accountLocation) {
|
||||
var urlHash = sha256hash(accountLocation.toExternalForm());
|
||||
var hash = base32Encode(Arrays.copyOfRange(urlHash, 0, 10));
|
||||
return "_" + hash.toLowerCase(Locale.ENGLISH)
|
||||
+ "._acme-challenge";
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean acceptable(String type) {
|
||||
return TYPE.equals(type);
|
||||
}
|
||||
|
||||
}
|
|
@ -25,6 +25,7 @@ import org.shredzone.acme4j.Login;
|
|||
import org.shredzone.acme4j.Session;
|
||||
import org.shredzone.acme4j.challenge.Challenge;
|
||||
import org.shredzone.acme4j.challenge.Dns01Challenge;
|
||||
import org.shredzone.acme4j.challenge.DnsAccount01Challenge;
|
||||
import org.shredzone.acme4j.challenge.Http01Challenge;
|
||||
import org.shredzone.acme4j.challenge.TlsAlpn01Challenge;
|
||||
import org.shredzone.acme4j.challenge.TokenChallenge;
|
||||
|
@ -83,6 +84,7 @@ public abstract class AbstractAcmeProvider implements AcmeProvider {
|
|||
var map = new HashMap<String, ChallengeProvider>();
|
||||
|
||||
map.put(Dns01Challenge.TYPE, Dns01Challenge::new);
|
||||
map.put(DnsAccount01Challenge.TYPE, DnsAccount01Challenge::new);
|
||||
map.put(Http01Challenge.TYPE, Http01Challenge::new);
|
||||
map.put(TlsAlpn01Challenge.TYPE, TlsAlpn01Challenge::new);
|
||||
|
||||
|
|
|
@ -72,6 +72,8 @@ public final class AcmeUtils {
|
|||
private static final Base64.Encoder URL_ENCODER = Base64.getUrlEncoder().withoutPadding();
|
||||
private static final Base64.Decoder URL_DECODER = Base64.getUrlDecoder();
|
||||
|
||||
private static final char[] BASE32_ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567".toCharArray();
|
||||
|
||||
/**
|
||||
* Enumeration of PEM labels.
|
||||
*/
|
||||
|
@ -154,6 +156,59 @@ public final class AcmeUtils {
|
|||
return URL_DECODER.decode(base64);
|
||||
}
|
||||
|
||||
/**
|
||||
* Base32 encodes a byte array.
|
||||
*
|
||||
* @param data Byte array to encode
|
||||
* @return Base32 encoded data (includes padding)
|
||||
* @since 3.6.0
|
||||
*/
|
||||
public static String base32Encode(byte[] data) {
|
||||
var result = new StringBuilder();
|
||||
var unconverted = new int[5];
|
||||
var converted = new int[8];
|
||||
|
||||
for (var ix = 0; ix < (data.length + 4) / 5; ix++) {
|
||||
var blocklen = unconverted.length;
|
||||
for (var pos = 0; pos < unconverted.length; pos++) {
|
||||
if ((ix * 5 + pos) < data.length) {
|
||||
unconverted[pos] = data[ix * 5 + pos] & 0xFF;
|
||||
} else {
|
||||
unconverted[pos] = 0;
|
||||
blocklen--;
|
||||
}
|
||||
}
|
||||
|
||||
converted[0] = (unconverted[0] >> 3) & 0x1F;
|
||||
converted[1] = ((unconverted[0] & 0x07) << 2) | ((unconverted[1] >> 6) & 0x03);
|
||||
converted[2] = (unconverted[1] >> 1) & 0x1F;
|
||||
converted[3] = ((unconverted[1] & 0x01) << 4) | ((unconverted[2] >> 4) & 0x0F);
|
||||
converted[4] = ((unconverted[2] & 0x0F) << 1) | ((unconverted[3] >> 7) & 0x01);
|
||||
converted[5] = (unconverted[3] >> 2) & 0x1F;
|
||||
converted[6] = ((unconverted[3] & 0x03) << 3) | ((unconverted[4] >> 5) & 0x07);
|
||||
converted[7] = unconverted[4] & 0x1F;
|
||||
|
||||
var padding = switch (blocklen) {
|
||||
case 1 -> 6;
|
||||
case 2 -> 4;
|
||||
case 3 -> 3;
|
||||
case 4 -> 1;
|
||||
case 5 -> 0;
|
||||
default -> throw new IllegalArgumentException("blocklen " + blocklen + " out of range");
|
||||
};
|
||||
|
||||
Arrays.stream(converted)
|
||||
.limit(converted.length - padding)
|
||||
.map(v -> BASE32_ALPHABET[v])
|
||||
.forEach(v -> result.append((char) v));
|
||||
|
||||
if (padding > 0) {
|
||||
result.append("=".repeat(padding));
|
||||
}
|
||||
}
|
||||
return result.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates that the given {@link String} is a valid base64url encoded value.
|
||||
*
|
||||
|
|
|
@ -88,7 +88,7 @@ public class ChallengeTest {
|
|||
@Test
|
||||
public void testNotAcceptable() {
|
||||
assertThrows(AcmeProtocolException.class, () ->
|
||||
new Http01Challenge(TestUtils.login(), getJSON("dnsChallenge"))
|
||||
new Http01Challenge(TestUtils.login(), getJSON("dns01Challenge"))
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -38,29 +38,22 @@ public class Dns01ChallengeTest {
|
|||
*/
|
||||
@Test
|
||||
public void testDnsChallenge() {
|
||||
var challenge = new Dns01Challenge(login, getJSON("dnsChallenge"));
|
||||
var challenge = new Dns01Challenge(login, getJSON("dns01Challenge"));
|
||||
|
||||
assertThat(challenge.getType()).isEqualTo(Dns01Challenge.TYPE);
|
||||
assertThat(challenge.getStatus()).isEqualTo(Status.PENDING);
|
||||
assertThat(challenge.getDigest()).isEqualTo("rzMmotrIgsithyBYc0vgiLUEEKYx0WetQRgEF2JIozA");
|
||||
assertThat(challenge.getAuthorization()).isEqualTo("pNvmJivs0WCko2suV7fhe-59oFqyYx_yB7tx6kIMAyE.HnWjTDnyqlCrm6tZ-6wX-TrEXgRdeNu9G71gqxSO6o0");
|
||||
|
||||
assertThat(challenge.getRRName("www.example.org")).isEqualTo("_acme-challenge.www.example.org.");
|
||||
assertThat(challenge.getRRName(Identifier.dns("www.example.org"))).isEqualTo("_acme-challenge.www.example.org.");
|
||||
assertThatExceptionOfType(AcmeProtocolException.class)
|
||||
.isThrownBy(() -> challenge.getRRName(Identifier.ip("127.0.0.10")));
|
||||
|
||||
var response = new JSONBuilder();
|
||||
challenge.prepareResponse(response);
|
||||
|
||||
assertThatJson(response.toString()).isEqualTo("{}");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testToRRName() {
|
||||
assertThat(Dns01Challenge.toRRName("www.example.org"))
|
||||
.isEqualTo("_acme-challenge.www.example.org.");
|
||||
assertThat(Dns01Challenge.toRRName(Identifier.dns("www.example.org")))
|
||||
.isEqualTo("_acme-challenge.www.example.org.");
|
||||
assertThatExceptionOfType(AcmeProtocolException.class)
|
||||
.isThrownBy(() -> Dns01Challenge.toRRName(Identifier.ip("127.0.0.10")));
|
||||
assertThat(Dns01Challenge.RECORD_NAME_PREFIX)
|
||||
.isEqualTo("_acme-challenge");
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -0,0 +1,61 @@
|
|||
/*
|
||||
* acme4j - Java ACME client
|
||||
*
|
||||
* Copyright (C) 2025 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 net.javacrumbs.jsonunit.assertj.JsonAssertions.assertThatJson;
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
|
||||
import static org.shredzone.acme4j.toolbox.TestUtils.getJSON;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.shredzone.acme4j.Identifier;
|
||||
import org.shredzone.acme4j.Login;
|
||||
import org.shredzone.acme4j.Status;
|
||||
import org.shredzone.acme4j.exception.AcmeProtocolException;
|
||||
import org.shredzone.acme4j.toolbox.JSONBuilder;
|
||||
import org.shredzone.acme4j.toolbox.TestUtils;
|
||||
|
||||
/**
|
||||
* Unit tests for {@link DnsAccount01Challenge}.
|
||||
*/
|
||||
class DnsAccount01ChallengeTest {
|
||||
|
||||
private final Login login = TestUtils.login();
|
||||
|
||||
/**
|
||||
* Test that {@link DnsAccount01Challenge} generates a correct authorization key.
|
||||
*/
|
||||
@Test
|
||||
public void testDnsChallenge() {
|
||||
var challenge = new DnsAccount01Challenge(login, getJSON("dnsAccount01Challenge"));
|
||||
|
||||
assertThat(challenge.getType()).isEqualTo(DnsAccount01Challenge.TYPE);
|
||||
assertThat(challenge.getStatus()).isEqualTo(Status.PENDING);
|
||||
assertThat(challenge.getDigest()).isEqualTo("MSB8ZUQOmbNfHors7PG580PBz4f9hDuOPDN_j1bNcXI");
|
||||
assertThat(challenge.getAuthorization()).isEqualTo("ODE4OWY4NTktYjhmYS00YmY1LTk5MDgtZTFjYTZmNjZlYTUx.HnWjTDnyqlCrm6tZ-6wX-TrEXgRdeNu9G71gqxSO6o0");
|
||||
|
||||
assertThat(challenge.getRRName("www.example.org"))
|
||||
.isEqualTo("_agozs7u2dml4wbyd._acme-challenge.www.example.org.");
|
||||
assertThat(challenge.getRRName(Identifier.dns("www.example.org")))
|
||||
.isEqualTo("_agozs7u2dml4wbyd._acme-challenge.www.example.org.");
|
||||
assertThatExceptionOfType(AcmeProtocolException.class)
|
||||
.isThrownBy(() -> challenge.getRRName(Identifier.ip("127.0.0.10")));
|
||||
|
||||
var response = new JSONBuilder();
|
||||
challenge.prepareResponse(response);
|
||||
|
||||
assertThatJson(response.toString()).isEqualTo("{}");
|
||||
}
|
||||
|
||||
}
|
|
@ -32,6 +32,7 @@ import org.shredzone.acme4j.Login;
|
|||
import org.shredzone.acme4j.Session;
|
||||
import org.shredzone.acme4j.challenge.Challenge;
|
||||
import org.shredzone.acme4j.challenge.Dns01Challenge;
|
||||
import org.shredzone.acme4j.challenge.DnsAccount01Challenge;
|
||||
import org.shredzone.acme4j.challenge.Http01Challenge;
|
||||
import org.shredzone.acme4j.challenge.TlsAlpn01Challenge;
|
||||
import org.shredzone.acme4j.challenge.TokenChallenge;
|
||||
|
@ -243,13 +244,17 @@ public class AbstractAcmeProviderTest {
|
|||
var c2 = provider.createChallenge(login, getJSON("httpChallenge"));
|
||||
assertThat(c2).isNotSameAs(c1);
|
||||
|
||||
var c3 = provider.createChallenge(login, getJSON("dnsChallenge"));
|
||||
var c3 = provider.createChallenge(login, getJSON("dns01Challenge"));
|
||||
assertThat(c3).isNotNull();
|
||||
assertThat(c3).isInstanceOf(Dns01Challenge.class);
|
||||
|
||||
var c4 = provider.createChallenge(login, getJSON("tlsAlpnChallenge"));
|
||||
var c4 = provider.createChallenge(login, getJSON("dnsAccount01Challenge"));
|
||||
assertThat(c4).isNotNull();
|
||||
assertThat(c4).isInstanceOf(TlsAlpn01Challenge.class);
|
||||
assertThat(c4).isInstanceOf(DnsAccount01Challenge.class);
|
||||
|
||||
var c5 = provider.createChallenge(login, getJSON("tlsAlpnChallenge"));
|
||||
assertThat(c5).isNotNull();
|
||||
assertThat(c5).isInstanceOf(TlsAlpn01Challenge.class);
|
||||
|
||||
var json6 = new JSONBuilder()
|
||||
.put("type", "foobar-01")
|
||||
|
|
|
@ -13,6 +13,7 @@
|
|||
*/
|
||||
package org.shredzone.acme4j.toolbox;
|
||||
|
||||
import static java.nio.charset.StandardCharsets.UTF_8;
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.within;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
|
@ -35,6 +36,7 @@ import org.junit.jupiter.api.BeforeAll;
|
|||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.params.ParameterizedTest;
|
||||
import org.junit.jupiter.params.provider.Arguments;
|
||||
import org.junit.jupiter.params.provider.CsvSource;
|
||||
import org.junit.jupiter.params.provider.MethodSource;
|
||||
import org.junit.jupiter.params.provider.NullSource;
|
||||
import org.junit.jupiter.params.provider.ValueSource;
|
||||
|
@ -88,6 +90,23 @@ public class AcmeUtilsTest {
|
|||
assertThat(base64UrlDecode).isEqualTo(sha256hash("foobar"));
|
||||
}
|
||||
|
||||
/**
|
||||
* Test base32 encode.
|
||||
*/
|
||||
@ParameterizedTest
|
||||
@CsvSource({ // Test vectors according to RFC 4648 section 10
|
||||
"'',''",
|
||||
"f,MY======",
|
||||
"fo,MZXQ====",
|
||||
"foo,MZXW6===",
|
||||
"foob,MZXW6YQ=",
|
||||
"fooba,MZXW6YTB",
|
||||
"foobar,MZXW6YTBOI======",
|
||||
})
|
||||
public void testBase32Encode(String unencoded, String encoded) {
|
||||
assertThat(base32Encode(unencoded.getBytes(UTF_8))).isEqualTo(encoded);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test base64 URL validation for valid values
|
||||
*/
|
||||
|
|
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"type": "dns-account-01",
|
||||
"url": "https://example.com/acme/chall/i00MGYwLWIx",
|
||||
"status": "pending",
|
||||
"token": "ODE4OWY4NTktYjhmYS00YmY1LTk5MDgtZTFjYTZmNjZlYTUx"
|
||||
}
|
|
@ -362,12 +362,12 @@ public class ClientTest {
|
|||
// Output the challenge, wait for acknowledge...
|
||||
LOG.info("Please create a TXT record:");
|
||||
LOG.info("{} IN TXT {}",
|
||||
Dns01Challenge.toRRName(auth.getIdentifier()), challenge.getDigest());
|
||||
challenge.getRRName(auth.getIdentifier()), challenge.getDigest());
|
||||
LOG.info("If you're ready, dismiss the dialog...");
|
||||
|
||||
StringBuilder message = new StringBuilder();
|
||||
message.append("Please create a TXT record:\n\n");
|
||||
message.append(Dns01Challenge.toRRName(auth.getIdentifier()))
|
||||
message.append(challenge.getRRName(auth.getIdentifier()))
|
||||
.append(" IN TXT ")
|
||||
.append(challenge.getDigest());
|
||||
acceptChallenge(message.toString());
|
||||
|
|
|
@ -23,6 +23,7 @@ import java.time.Duration;
|
|||
import java.time.Instant;
|
||||
import java.time.temporal.ChronoUnit;
|
||||
|
||||
import org.junit.jupiter.api.Disabled;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.params.ParameterizedTest;
|
||||
import org.junit.jupiter.params.provider.NullSource;
|
||||
|
@ -36,6 +37,7 @@ import org.shredzone.acme4j.Session;
|
|||
import org.shredzone.acme4j.Status;
|
||||
import org.shredzone.acme4j.challenge.Challenge;
|
||||
import org.shredzone.acme4j.challenge.Dns01Challenge;
|
||||
import org.shredzone.acme4j.challenge.DnsAccount01Challenge;
|
||||
import org.shredzone.acme4j.challenge.Http01Challenge;
|
||||
import org.shredzone.acme4j.challenge.TlsAlpn01Challenge;
|
||||
import org.shredzone.acme4j.exception.AcmeException;
|
||||
|
@ -81,7 +83,30 @@ public class OrderIT extends PebbleITBase {
|
|||
|
||||
var challenge = auth.findChallenge(Dns01Challenge.class).orElseThrow();
|
||||
|
||||
var challengeDomainName = Dns01Challenge.toRRName(auth.getIdentifier());
|
||||
var challengeDomainName = challenge.getRRName(auth.getIdentifier());
|
||||
|
||||
client.dnsAddTxtRecord(challengeDomainName, challenge.getDigest());
|
||||
|
||||
cleanup(() -> client.dnsRemoveTxtRecord(challengeDomainName));
|
||||
|
||||
return challenge;
|
||||
}, OrderIT::standardRevoker, profile);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test if a certificate can be ordered via dns-account-01 challenge.
|
||||
*/
|
||||
@ParameterizedTest
|
||||
@NullSource
|
||||
@ValueSource(strings = {"default", "shortlived"})
|
||||
@Disabled("Waiting for https://github.com/letsencrypt/pebble/pull/489")
|
||||
public void testDnsAccountValidation(String profile) throws Exception {
|
||||
orderCertificate(TEST_DOMAIN, auth -> {
|
||||
var client = getBammBammClient();
|
||||
|
||||
var challenge = auth.findChallenge(DnsAccount01Challenge.class).orElseThrow();
|
||||
|
||||
var challengeDomainName = challenge.getRRName(auth.getIdentifier());
|
||||
|
||||
client.dnsAddTxtRecord(challengeDomainName, challenge.getDigest());
|
||||
|
||||
|
|
|
@ -2,16 +2,15 @@
|
|||
|
||||
With the `dns-01` challenge, you prove to the CA that you are able to control the DNS records of the domain to be authorized, by creating a TXT record with a signed content.
|
||||
|
||||
`Dns01Challenge` provides a digest string:
|
||||
`Dns01Challenge` provides a resource record name and a digest string:
|
||||
|
||||
```java
|
||||
Dns01Challenge challenge = auth.findChallenge(Dns01Challenge.class);
|
||||
String domain = auth.getIdentifier().getDomain();
|
||||
|
||||
String resourceName = Dns01Challenge.toRRName(auth.getIdentifier());
|
||||
String resourceName = challenge.getRRName(auth.getIdentifier());
|
||||
String digest = challenge.getDigest();
|
||||
```
|
||||
|
||||
The CA expects a TXT record at `_acme-challenge.${domain}.` with the `digest` string as value. The `Dns01Challenge.toRRName()` method converts the domain name to a resource record name (including the trailing full stop, e.g. `_acme-challenge.www.example.org.`). The `_acme-challenge` prefix is also available as constant (`Dns01Challenge.RECORD_NAME_PREFIX`).
|
||||
The CA expects a TXT record at `resourceName` with the `digest` string as value. The `Dns01Challenge.getRRName()` method converts the domain name to a resource record name (including the trailing full stop, e.g. `_acme-challenge.www.example.org.`).
|
||||
|
||||
The validation was successful if the CA was able to fetch the TXT record and got the correct `digest` returned.
|
||||
|
|
|
@ -0,0 +1,23 @@
|
|||
# dns-account-01 Challenge
|
||||
|
||||
With the `dns-account-01` challenge, you prove to the CA that you are able to control the DNS records of the domain to be authorized, by creating a TXT record with a signed content.
|
||||
|
||||
This challenge is specified in [draft-ietf-acme-dns-account-label-00](https://datatracker.ietf.org/doc/draft-ietf-acme-dns-account-label/).
|
||||
|
||||
!!! warning
|
||||
The support of this challenge is **experimental**. The implementation is only unit tested for compliance with the specification, but is not integration tested yet. There may be breaking changes in this part of the API in future releases.
|
||||
|
||||
With the `dns-account-01` challenge, you prove to the CA that you are able to control the DNS records of the domain to be authorized, by creating a TXT record with a signed content.
|
||||
|
||||
`DnsAccount01Challenge` provides a digest string and a resource record name:
|
||||
|
||||
```java
|
||||
DnsAccount01Challenge challenge = auth.findChallenge(DnsAccount01Challenge.class);
|
||||
|
||||
String resourceRecordName = challenge.getRRName(auth.getIdentifier());
|
||||
String digest = challenge.getDigest();
|
||||
```
|
||||
|
||||
The CA expects a TXT record at `resourceRecordName` with the `digest` string as value. The `getRRName()` method converts the domain name to a resource record name (including the trailing full stop).
|
||||
|
||||
The validation was successful if the CA was able to fetch the TXT record and got the correct `digest` returned.
|
|
@ -6,10 +6,11 @@ There are different kinds of challenges. The most simple is maybe the HTTP chall
|
|||
|
||||
The ACME specifications define these standard challenges:
|
||||
|
||||
* [http-01](http-01.md)
|
||||
* [dns-01](dns-01.md)
|
||||
* [http-01](http-01.md)
|
||||
|
||||
_acme4j_ also supports these non-standard challenges:
|
||||
|
||||
* [tls-alpn-01](tls-alpn-01.md)
|
||||
* [dns-account-01](dns-account-01.md) ([draft-ietf-acme-dns-account-label-00](https://datatracker.ietf.org/doc/draft-ietf-acme-dns-account-label/00/))
|
||||
* [email-reply-00](email-reply-00.md)
|
||||
* [tls-alpn-01](tls-alpn-01.md)
|
||||
|
|
|
@ -340,12 +340,12 @@ public Challenge dnsChallenge(Authorization auth) throws AcmeException {
|
|||
// Output the challenge, wait for acknowledge...
|
||||
LOG.info("Please create a TXT record:");
|
||||
LOG.info("{} IN TXT {}",
|
||||
Dns01Challenge.toRRName(auth.getIdentifier()), challenge.getDigest());
|
||||
challenge.getRRName(auth.getIdentifier()), challenge.getDigest());
|
||||
LOG.info("If you're ready, dismiss the dialog...");
|
||||
|
||||
StringBuilder message = new StringBuilder();
|
||||
message.append("Please create a TXT record:\n\n");
|
||||
message.append(Dns01Challenge.toRRName(auth.getIdentifier()))
|
||||
message.append(challenge.getRRName(auth.getIdentifier()))
|
||||
.append(" IN TXT ")
|
||||
.append(challenge.getDigest());
|
||||
acceptChallenge(message.toString());
|
||||
|
|
|
@ -21,6 +21,7 @@ Latest version:  for subdomain validation
|
||||
* Supports [draft-ietf-acme-ari-07](https://www.ietf.org/archive/id/draft-ietf-acme-ari-07.html) for renewal information (experimental)
|
||||
* Supports [draft-aaron-acme-profiles-00](https://www.ietf.org/archive/id/draft-aaron-acme-profiles-00.html) for certificate profiles (experimental)
|
||||
* Supports [draft-ietf-acme-dns-account-label-00](https://datatracker.ietf.org/doc/draft-ietf-acme-dns-account-label/) for DNS labeled with ACME account ID challenges (experimental)
|
||||
* Easy to use Java API
|
||||
* Requires JRE 17 or higher
|
||||
* Supports [Buypass](https://buypass.com/), [Google Trust Services](https://pki.goog/), [Let's Encrypt](https://letsencrypt.org/), [SSL.com](https://www.ssl.com/), [ZeroSSL](https://zerossl.com/), and all other CAs that comply with the ACME protocol (RFC 8555). Note that _acme4j_ is an independent project that is not supported or endorsed by any of the CAs.
|
||||
|
|
|
@ -5,6 +5,7 @@ This document will help you migrate your code to the latest _acme4j_ version.
|
|||
## Migration to Version 3.6.0
|
||||
|
||||
- _acme4j_ requires JRE 17 or higher now.
|
||||
- In order to keep the API consistent, the static method `Dns01Challenge.toRRName()` is replaced with a class method `Dns01Challenge.getRRName()`. The static method is marked as deprecated, but is still functional.
|
||||
|
||||
## Migration to Version 3.5.0
|
||||
|
||||
|
|
|
@ -37,10 +37,11 @@ nav:
|
|||
- 'faq.md'
|
||||
- Challenges:
|
||||
- 'challenge/index.md'
|
||||
- 'challenge/http-01.md'
|
||||
- 'challenge/dns-01.md'
|
||||
- 'challenge/tls-alpn-01.md'
|
||||
- 'challenge/dns-account-01.md'
|
||||
- 'challenge/email-reply-00.md'
|
||||
- 'challenge/http-01.md'
|
||||
- 'challenge/tls-alpn-01.md'
|
||||
- CA:
|
||||
- 'ca/index.md'
|
||||
- 'ca/buypass.md'
|
||||
|
|
Loading…
Reference in New Issue