mirror of https://github.com/shred/acme4j
Support the acme-star extension
parent
f609a797cb
commit
d0d93b855a
|
@ -13,6 +13,7 @@ It is an independent open source implementation that is not affiliated with or e
|
|||
* Fully supports the ACME v2 protocol up to [draft 13](https://tools.ietf.org/html/draft-ietf-acme-acme-13)
|
||||
* Supports all ACME challenges and the `tls-alpn-01` challenge
|
||||
* Supports the [acme-ip draft](https://tools.ietf.org/html/draft-ietf-acme-ip)
|
||||
* Supports the [acme-star draft](https://tools.ietf.org/html/draft-ietf-acme-star) for short-term automatic certificate renewal (experimental)
|
||||
* Easy to use Java API
|
||||
* Requires JRE 8 (update 101) or higher
|
||||
* Built with maven, packages available at [Maven Central](http://search.maven.org/#search|ga|1|g%3A%22org.shredzone.acme4j%22)
|
||||
|
|
|
@ -17,6 +17,7 @@ import static java.util.stream.Collectors.toList;
|
|||
|
||||
import java.net.URI;
|
||||
import java.net.URL;
|
||||
import java.time.Duration;
|
||||
import java.util.Collection;
|
||||
|
||||
import javax.annotation.CheckForNull;
|
||||
|
@ -82,6 +83,36 @@ public class Metadata {
|
|||
return meta.get("externalAccountRequired").map(Value::asBoolean).orElse(false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether the CA supports short-term auto renewal of certificates.
|
||||
*
|
||||
* @since 2.3
|
||||
*/
|
||||
public boolean isStarEnabled() {
|
||||
return meta.get("star-enabled").map(Value::asBoolean).orElse(false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the minimum acceptable value for the maximum validity of a certificate
|
||||
* before auto renewal. {@code null} if the CA does not support short-term auto
|
||||
* renewal.
|
||||
*
|
||||
* @since 2.3
|
||||
*/
|
||||
public Duration getStarMinCertValidity() {
|
||||
return meta.get("star-min-cert-validity").map(Value::asDuration).orElse(null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the maximum delta between recurrent end date and recurrent start date.
|
||||
* {@code null} if the CA does not support short-term auto renewal.
|
||||
*
|
||||
* @since 2.3
|
||||
*/
|
||||
public Duration getStarMaxRenewal() {
|
||||
return meta.get("star-max-renewal").map(Value::asDuration).orElse(null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the JSON representation of the metadata. This is useful for reading
|
||||
* proprietary metadata properties.
|
||||
|
|
|
@ -16,6 +16,7 @@ package org.shredzone.acme4j;
|
|||
import static java.util.stream.Collectors.toList;
|
||||
|
||||
import java.net.URL;
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
@ -25,6 +26,7 @@ import javax.annotation.ParametersAreNonnullByDefault;
|
|||
|
||||
import org.shredzone.acme4j.connector.Connection;
|
||||
import org.shredzone.acme4j.exception.AcmeException;
|
||||
import org.shredzone.acme4j.toolbox.JSON;
|
||||
import org.shredzone.acme4j.toolbox.JSON.Value;
|
||||
import org.shredzone.acme4j.toolbox.JSONBuilder;
|
||||
import org.slf4j.Logger;
|
||||
|
@ -169,4 +171,81 @@ public class Order extends AcmeJsonResource {
|
|||
invalidate();
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if this order is recurrent, according to the ACME STAR specifications.
|
||||
*
|
||||
* @since 2.3
|
||||
*/
|
||||
public boolean isRecurrent() {
|
||||
return getJSON().get("recurrent")
|
||||
.optional()
|
||||
.map(Value::asBoolean)
|
||||
.orElse(false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the earliest date of validity of the first certificate issued, or
|
||||
* {@code null}.
|
||||
*
|
||||
* @since 2.3
|
||||
*/
|
||||
@CheckForNull
|
||||
public Instant getRecurrentStart() {
|
||||
return getJSON().get("recurrent-start-date")
|
||||
.optional()
|
||||
.map(Value::asInstant)
|
||||
.orElse(null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the latest date of validity of the last certificate issued, or
|
||||
* {@code null}.
|
||||
*
|
||||
* @since 2.3
|
||||
*/
|
||||
@CheckForNull
|
||||
public Instant getRecurrentEnd() {
|
||||
return getJSON().get("recurrent-end-date")
|
||||
.optional()
|
||||
.map(Value::asInstant)
|
||||
.orElse(null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the maximum validity period of each certificate, or {@code null}.
|
||||
*
|
||||
* @since 2.3
|
||||
*/
|
||||
@CheckForNull
|
||||
public Duration getRecurrentCertificateValidity() {
|
||||
return getJSON().get("recurrent-certificate-validity")
|
||||
.optional()
|
||||
.map(Value::asDuration)
|
||||
.orElse(null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Cancels a recurrent order.
|
||||
*
|
||||
* @since 2.3
|
||||
*/
|
||||
public void cancelRecurrent() throws AcmeException {
|
||||
if (!getSession().getMetadata().isStarEnabled()) {
|
||||
throw new AcmeException("CA does not support short-term automatic renewals");
|
||||
}
|
||||
|
||||
LOG.debug("cancel");
|
||||
try (Connection conn = connect()) {
|
||||
JSONBuilder claims = new JSONBuilder();
|
||||
claims.put("status", "canceled");
|
||||
|
||||
conn.sendSignedRequest(getLocation(), claims, getLogin());
|
||||
|
||||
JSON json = conn.readJsonResponse();
|
||||
if (json != null) {
|
||||
setJSON(json);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -17,6 +17,7 @@ import static java.util.Objects.requireNonNull;
|
|||
import static java.util.stream.Collectors.toList;
|
||||
|
||||
import java.net.URL;
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.util.Collection;
|
||||
import java.util.LinkedHashSet;
|
||||
|
@ -45,6 +46,10 @@ public class OrderBuilder {
|
|||
private final Set<Identifier> identifierSet = new LinkedHashSet<>();
|
||||
private Instant notBefore;
|
||||
private Instant notAfter;
|
||||
private boolean recurrent;
|
||||
private Instant recurrentStart;
|
||||
private Instant recurrentEnd;
|
||||
private Duration recurrentValidity;
|
||||
|
||||
/**
|
||||
* Create a new {@link OrderBuilder}.
|
||||
|
@ -131,6 +136,9 @@ public class OrderBuilder {
|
|||
* @return itself
|
||||
*/
|
||||
public OrderBuilder notBefore(Instant notBefore) {
|
||||
if (recurrent) {
|
||||
throw new IllegalArgumentException("cannot combine notBefore with recurrent");
|
||||
}
|
||||
this.notBefore = requireNonNull(notBefore, "notBefore");
|
||||
return this;
|
||||
}
|
||||
|
@ -142,10 +150,84 @@ public class OrderBuilder {
|
|||
* @return itself
|
||||
*/
|
||||
public OrderBuilder notAfter(Instant notAfter) {
|
||||
if (recurrent) {
|
||||
throw new IllegalArgumentException("cannot combine notAfter with recurrent");
|
||||
}
|
||||
this.notAfter = requireNonNull(notAfter, "notAfter");
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Enables short-term automatic renewal of the certificate. Must be supported by the
|
||||
* CA.
|
||||
* <p>
|
||||
* Recurrent renewals cannot be combined with {@link #notBefore(Instant)} or
|
||||
* {@link #notAfter(Instant)}.
|
||||
*
|
||||
* @return itself
|
||||
* @since 2.3
|
||||
*/
|
||||
public OrderBuilder recurrent() {
|
||||
if (notBefore != null || notAfter != null) {
|
||||
throw new IllegalArgumentException("cannot combine notBefore/notAfter with recurrent");
|
||||
}
|
||||
this.recurrent = true;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the earliest date of validity of the first issued certificate. If not set,
|
||||
* the start date is the earliest possible date.
|
||||
* <p>
|
||||
* Implies {@link #recurrent()}.
|
||||
*
|
||||
* @param start
|
||||
* Start date of validity
|
||||
* @return itself
|
||||
* @since 2.3
|
||||
*/
|
||||
public OrderBuilder recurrentStart(Instant start) {
|
||||
recurrent();
|
||||
this.recurrentStart = requireNonNull(start, "start");
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the latest date of validity of the last issued certificate. If not set, the
|
||||
* CA's default is used.
|
||||
* <p>
|
||||
* Implies {@link #recurrent()}.
|
||||
*
|
||||
* @param end
|
||||
* End date of validity
|
||||
* @return itself
|
||||
* @see Metadata#getStarMaxRenewal()
|
||||
* @since 2.3
|
||||
*/
|
||||
public OrderBuilder recurrentEnd(Instant end) {
|
||||
recurrent();
|
||||
this.recurrentEnd = requireNonNull(end, "end");
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the maximum validity period of each certificate. If not set, the CA's
|
||||
* default is used.
|
||||
* <p>
|
||||
* Implies {@link #recurrent()}.
|
||||
*
|
||||
* @param duration
|
||||
* Duration of validity of each certificate
|
||||
* @return itself
|
||||
* @see Metadata#getStarMinCertValidity()
|
||||
* @since 2.3
|
||||
*/
|
||||
public OrderBuilder recurrentCertificateValidity(Duration duration) {
|
||||
recurrent();
|
||||
this.recurrentValidity = requireNonNull(duration, "duration");
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a new order to the server, and returns an {@link Order} object.
|
||||
*
|
||||
|
@ -158,6 +240,10 @@ public class OrderBuilder {
|
|||
|
||||
Session session = login.getSession();
|
||||
|
||||
if (recurrent && !session.getMetadata().isStarEnabled()) {
|
||||
throw new AcmeException("CA does not support short-term automatic renewals");
|
||||
}
|
||||
|
||||
LOG.debug("create");
|
||||
try (Connection conn = session.provider().connect()) {
|
||||
JSONBuilder claims = new JSONBuilder();
|
||||
|
@ -170,6 +256,19 @@ public class OrderBuilder {
|
|||
claims.put("notAfter", notAfter);
|
||||
}
|
||||
|
||||
if (recurrent) {
|
||||
claims.put("recurrent", true);
|
||||
if (recurrentStart != null) {
|
||||
claims.put("recurrent-start-date", recurrentStart);
|
||||
}
|
||||
if (recurrentStart != null) {
|
||||
claims.put("recurrent-end-date", recurrentEnd);
|
||||
}
|
||||
if (recurrentValidity != null) {
|
||||
claims.put("recurrent-certificate-validity", recurrentValidity);
|
||||
}
|
||||
}
|
||||
|
||||
conn.sendSignedRequest(session.resourceUrl(Resource.NEW_ORDER), claims, login);
|
||||
|
||||
URL orderLocation = conn.getLocation();
|
||||
|
|
|
@ -67,6 +67,13 @@ public enum Status {
|
|||
*/
|
||||
EXPIRED,
|
||||
|
||||
/**
|
||||
* A recurrent {@link Order} is canceled.
|
||||
*
|
||||
* @since 2.3
|
||||
*/
|
||||
CANCELED,
|
||||
|
||||
/**
|
||||
* The server did not provide a status, or the provided status is not a specified ACME
|
||||
* status.
|
||||
|
|
|
@ -27,6 +27,7 @@ import java.net.MalformedURLException;
|
|||
import java.net.URI;
|
||||
import java.net.URISyntaxException;
|
||||
import java.net.URL;
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
|
@ -438,6 +439,20 @@ public final class JSON implements Serializable {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the value as {@link Duration}.
|
||||
*
|
||||
* @since 2.3
|
||||
*/
|
||||
public Duration asDuration() {
|
||||
required();
|
||||
try {
|
||||
return Duration.ofSeconds(((Number) val).longValue());
|
||||
} catch (ClassCastException ex) {
|
||||
throw new AcmeProtocolException(path + ": bad duration " + val, ex);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the value as base64 decoded byte array.
|
||||
*/
|
||||
|
|
|
@ -17,6 +17,7 @@ import static org.shredzone.acme4j.toolbox.AcmeUtils.base64UrlEncode;
|
|||
|
||||
import java.security.Key;
|
||||
import java.security.PublicKey;
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.Collection;
|
||||
|
@ -84,6 +85,27 @@ public class JSONBuilder {
|
|||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Puts a {@link Duration} to the JSON. If a property with the key exists, it will be
|
||||
* replaced.
|
||||
*
|
||||
* @param key
|
||||
* Property key
|
||||
* @param value
|
||||
* Property {@link Duration} value
|
||||
* @return {@code this}
|
||||
* @since 2.3
|
||||
*/
|
||||
public JSONBuilder put(String key, @Nullable Duration value) {
|
||||
if (value == null) {
|
||||
put(key, (Object) null);
|
||||
return this;
|
||||
}
|
||||
|
||||
put(key, value.getSeconds());
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Puts binary data to the JSON. The data is base64 url encoded.
|
||||
*
|
||||
|
|
|
@ -14,7 +14,7 @@
|
|||
package org.shredzone.acme4j;
|
||||
|
||||
import static org.hamcrest.Matchers.*;
|
||||
import static org.junit.Assert.assertThat;
|
||||
import static org.junit.Assert.*;
|
||||
import static org.shredzone.acme4j.toolbox.AcmeUtils.parseTimestamp;
|
||||
import static org.shredzone.acme4j.toolbox.TestUtils.*;
|
||||
import static uk.co.datumedge.hamcrest.json.SameJSONAs.sameJSONAs;
|
||||
|
@ -22,11 +22,13 @@ import static uk.co.datumedge.hamcrest.json.SameJSONAs.sameJSONAs;
|
|||
import java.net.HttpURLConnection;
|
||||
import java.net.InetAddress;
|
||||
import java.net.URL;
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.util.Arrays;
|
||||
|
||||
import org.junit.Test;
|
||||
import org.shredzone.acme4j.connector.Resource;
|
||||
import org.shredzone.acme4j.exception.AcmeException;
|
||||
import org.shredzone.acme4j.provider.TestableConnectionProvider;
|
||||
import org.shredzone.acme4j.toolbox.JSON;
|
||||
import org.shredzone.acme4j.toolbox.JSONBuilder;
|
||||
|
@ -112,4 +114,141 @@ public class OrderBuilderTest {
|
|||
provider.close();
|
||||
}
|
||||
|
||||
/**
|
||||
* Test that a new recurrent {@link Order} can be created.
|
||||
*/
|
||||
@Test
|
||||
public void testRecurrentOrderCertificate() throws Exception {
|
||||
Instant recurrentStart = parseTimestamp("2018-01-01T00:00:00Z");
|
||||
Instant recurrentEnd = parseTimestamp("2019-01-01T00:00:00Z");
|
||||
Duration validity = Duration.ofDays(7);
|
||||
|
||||
TestableConnectionProvider provider = new TestableConnectionProvider() {
|
||||
@Override
|
||||
public int sendSignedRequest(URL url, JSONBuilder claims, Login login) {
|
||||
assertThat(url, is(resourceUrl));
|
||||
assertThat(claims.toString(), sameJSONAs(getJSON("requestRecurrentOrderRequest").toString()));
|
||||
assertThat(login, is(notNullValue()));
|
||||
return HttpURLConnection.HTTP_CREATED;
|
||||
}
|
||||
|
||||
@Override
|
||||
public JSON readJsonResponse() {
|
||||
return getJSON("requestRecurrentOrderResponse");
|
||||
}
|
||||
|
||||
@Override
|
||||
public URL getLocation() {
|
||||
return locationUrl;
|
||||
}
|
||||
};
|
||||
|
||||
Login login = provider.createLogin();
|
||||
|
||||
provider.putMetadata("star-enabled", true);
|
||||
provider.putTestResource(Resource.NEW_ORDER, resourceUrl);
|
||||
|
||||
Account account = new Account(login);
|
||||
Order order = account.newOrder()
|
||||
.domain("example.org")
|
||||
.recurrent()
|
||||
.recurrentStart(recurrentStart)
|
||||
.recurrentEnd(recurrentEnd)
|
||||
.recurrentCertificateValidity(validity)
|
||||
.create();
|
||||
|
||||
assertThat(order.getIdentifiers(), containsInAnyOrder(Identifier.dns("example.org")));
|
||||
assertThat(order.getNotBefore(), is(nullValue()));
|
||||
assertThat(order.getNotAfter(), is(nullValue()));
|
||||
assertThat(order.isRecurrent(), is(true));
|
||||
assertThat(order.getRecurrentStart(), is(recurrentStart));
|
||||
assertThat(order.getRecurrentEnd(), is(recurrentEnd));
|
||||
assertThat(order.getRecurrentCertificateValidity(), is(validity));
|
||||
assertThat(order.getLocation(), is(locationUrl));
|
||||
|
||||
provider.close();
|
||||
}
|
||||
|
||||
/**
|
||||
* Test that a recurrent {@link Order} cannot be created if unsupported by the CA.
|
||||
*/
|
||||
@Test(expected = AcmeException.class)
|
||||
public void testRecurrentOrderCertificateFails() throws Exception {
|
||||
TestableConnectionProvider provider = new TestableConnectionProvider();
|
||||
provider.putTestResource(Resource.NEW_ORDER, resourceUrl);
|
||||
|
||||
Login login = provider.createLogin();
|
||||
|
||||
Account account = new Account(login);
|
||||
account.newOrder()
|
||||
.domain("example.org")
|
||||
.recurrent()
|
||||
.create();
|
||||
|
||||
provider.close();
|
||||
}
|
||||
|
||||
/**
|
||||
* Test that recurrent and notBefore/notAfter cannot be mixed.
|
||||
*/
|
||||
@Test
|
||||
public void testRecurrentNotMixed() throws Exception {
|
||||
Instant someInstant = parseTimestamp("2018-01-01T00:00:00Z");
|
||||
|
||||
TestableConnectionProvider provider = new TestableConnectionProvider();
|
||||
Login login = provider.createLogin();
|
||||
|
||||
Account account = new Account(login);
|
||||
|
||||
try {
|
||||
OrderBuilder ob = account.newOrder().recurrent();
|
||||
ob.notBefore(someInstant);
|
||||
fail("accepted notBefore");
|
||||
} catch (IllegalArgumentException ex) {
|
||||
// expected
|
||||
}
|
||||
|
||||
try {
|
||||
OrderBuilder ob = account.newOrder().recurrent();
|
||||
ob.notAfter(someInstant);
|
||||
fail("accepted notAfter");
|
||||
} catch (IllegalArgumentException ex) {
|
||||
// expected
|
||||
}
|
||||
|
||||
try {
|
||||
OrderBuilder ob = account.newOrder().notBefore(someInstant);
|
||||
ob.recurrent();
|
||||
fail("accepted recurrent");
|
||||
} catch (IllegalArgumentException ex) {
|
||||
// expected
|
||||
}
|
||||
|
||||
try {
|
||||
OrderBuilder ob = account.newOrder().notBefore(someInstant);
|
||||
ob.recurrentStart(someInstant);
|
||||
fail("accepted recurrentStart");
|
||||
} catch (IllegalArgumentException ex) {
|
||||
// expected
|
||||
}
|
||||
|
||||
try {
|
||||
OrderBuilder ob = account.newOrder().notBefore(someInstant);
|
||||
ob.recurrentEnd(someInstant);
|
||||
fail("accepted recurrentEnd");
|
||||
} catch (IllegalArgumentException ex) {
|
||||
// expected
|
||||
}
|
||||
|
||||
try {
|
||||
OrderBuilder ob = account.newOrder().notBefore(someInstant);
|
||||
ob.recurrentCertificateValidity(Duration.ofDays(7));
|
||||
fail("accepted recurrentCertificateValidity");
|
||||
} catch (IllegalArgumentException ex) {
|
||||
// expected
|
||||
}
|
||||
|
||||
provider.close();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -22,6 +22,7 @@ import static uk.co.datumedge.hamcrest.json.SameJSONAs.sameJSONAs;
|
|||
import java.net.HttpURLConnection;
|
||||
import java.net.URI;
|
||||
import java.net.URL;
|
||||
import java.time.Duration;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
|
@ -80,6 +81,11 @@ public class OrderTest {
|
|||
assertThat(order.getCertificate().getLocation(), is(url("https://example.com/acme/cert/1234")));
|
||||
assertThat(order.getFinalizeLocation(), is(finalizeUrl));
|
||||
|
||||
assertThat(order.isRecurrent(), is(false));
|
||||
assertThat(order.getRecurrentStart(), is(nullValue()));
|
||||
assertThat(order.getRecurrentEnd(), is(nullValue()));
|
||||
assertThat(order.getRecurrentCertificateValidity(), is(nullValue()));
|
||||
|
||||
assertThat(order.getError(), is(notNullValue()));
|
||||
assertThat(order.getError().getType(), is(URI.create("urn:ietf:params:acme:error:connection")));
|
||||
assertThat(order.getError().getDetail(), is("connection refused"));
|
||||
|
@ -202,4 +208,76 @@ public class OrderTest {
|
|||
provider.close();
|
||||
}
|
||||
|
||||
/**
|
||||
* Test that order is properly updated.
|
||||
*/
|
||||
@Test
|
||||
public void testRecurrentUpdate() throws Exception {
|
||||
TestableConnectionProvider provider = new TestableConnectionProvider() {
|
||||
@Override
|
||||
public void sendRequest(URL url, Session session) {
|
||||
assertThat(url, is(locationUrl));
|
||||
}
|
||||
|
||||
@Override
|
||||
public JSON readJsonResponse() {
|
||||
return getJSON("updateRecurrentOrderResponse");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleRetryAfter(String message) {
|
||||
assertThat(message, not(nullValue()));
|
||||
}
|
||||
};
|
||||
|
||||
provider.putMetadata("star-enabled", true);
|
||||
|
||||
Login login = provider.createLogin();
|
||||
|
||||
Order order = new Order(login, locationUrl);
|
||||
order.update();
|
||||
|
||||
assertThat(order.isRecurrent(), is(true));
|
||||
assertThat(order.getRecurrentStart(), is(parseTimestamp("2016-01-01T00:00:00Z")));
|
||||
assertThat(order.getRecurrentEnd(), is(parseTimestamp("2017-01-01T00:00:00Z")));
|
||||
assertThat(order.getRecurrentCertificateValidity(), is(Duration.ofHours(168)));
|
||||
assertThat(order.getNotBefore(), is(nullValue()));
|
||||
assertThat(order.getNotAfter(), is(nullValue()));
|
||||
|
||||
provider.close();
|
||||
}
|
||||
|
||||
/**
|
||||
* Test that recurrent order is properly canceled.
|
||||
*/
|
||||
@Test
|
||||
public void testCancel() throws Exception {
|
||||
TestableConnectionProvider provider = new TestableConnectionProvider() {
|
||||
@Override
|
||||
public int sendSignedRequest(URL url, JSONBuilder claims, Login login) {
|
||||
JSON json = claims.toJSON();
|
||||
assertThat(json.get("status").asString(), is("canceled"));
|
||||
assertThat(url, is(locationUrl));
|
||||
assertThat(login, is(notNullValue()));
|
||||
return HttpURLConnection.HTTP_OK;
|
||||
}
|
||||
|
||||
@Override
|
||||
public JSON readJsonResponse() {
|
||||
return getJSON("canceledOrderResponse");
|
||||
}
|
||||
};
|
||||
|
||||
provider.putMetadata("star-enabled", true);
|
||||
|
||||
Login login = provider.createLogin();
|
||||
|
||||
Order order = new Order(login, locationUrl);
|
||||
order.cancelRecurrent();
|
||||
|
||||
assertThat(order.getStatus(), is(Status.CANCELED));
|
||||
|
||||
provider.close();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -25,6 +25,7 @@ import java.net.Proxy.Type;
|
|||
import java.net.URI;
|
||||
import java.net.URL;
|
||||
import java.security.KeyPair;
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
|
||||
import org.junit.Test;
|
||||
|
@ -179,6 +180,9 @@ public class SessionTest {
|
|||
assertThat(meta.getTermsOfService(), is(nullValue()));
|
||||
assertThat(meta.getWebsite(), is(nullValue()));
|
||||
assertThat(meta.getCaaIdentities(), is(empty()));
|
||||
assertThat(meta.isStarEnabled(), is(false));
|
||||
assertThat(meta.getStarMaxRenewal(), is(nullValue()));
|
||||
assertThat(meta.getStarMinCertValidity(), is(nullValue()));
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -208,6 +212,9 @@ public class SessionTest {
|
|||
assertThat(meta.getTermsOfService(), is(URI.create("https://example.com/acme/terms")));
|
||||
assertThat(meta.getWebsite(), is(url("https://www.example.com/")));
|
||||
assertThat(meta.getCaaIdentities(), containsInAnyOrder("example.com"));
|
||||
assertThat(meta.isStarEnabled(), is(true));
|
||||
assertThat(meta.getStarMaxRenewal(), is(Duration.ofDays(365)));
|
||||
assertThat(meta.getStarMinCertValidity(), is(Duration.ofHours(24)));
|
||||
assertThat(meta.isExternalAccountRequired(), is(true));
|
||||
assertThat(meta.getJSON(), is(notNullValue()));
|
||||
}
|
||||
|
|
|
@ -38,6 +38,7 @@ public class TestableConnectionProvider extends DummyConnection implements AcmeP
|
|||
private final Map<String, BiFunction<Login, JSON, Challenge>> creatorMap = new HashMap<>();
|
||||
private final Map<String, Challenge> createdMap = new HashMap<>();
|
||||
private final JSONBuilder directory = new JSONBuilder();
|
||||
private JSONBuilder metadata = null;
|
||||
|
||||
/**
|
||||
* Register a {@link Resource} mapping.
|
||||
|
@ -51,6 +52,21 @@ public class TestableConnectionProvider extends DummyConnection implements AcmeP
|
|||
directory.put(r.path(), u);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a property to the metadata registry.
|
||||
*
|
||||
* @param key
|
||||
* Metadata key
|
||||
* @param value
|
||||
* Metadata value
|
||||
*/
|
||||
public void putMetadata(String key, Object value) {
|
||||
if (metadata == null) {
|
||||
metadata = directory.object("meta");
|
||||
}
|
||||
metadata.put(key, value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Register a {@link Challenge}.
|
||||
*
|
||||
|
|
|
@ -18,6 +18,7 @@ import static org.junit.Assert.assertThat;
|
|||
|
||||
import java.io.IOException;
|
||||
import java.security.KeyPair;
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.time.ZoneId;
|
||||
import java.time.ZonedDateTime;
|
||||
|
@ -82,12 +83,14 @@ public class JSONBuilderTest {
|
|||
@Test
|
||||
public void testDate() {
|
||||
Instant date = ZonedDateTime.of(2016, 6, 1, 5, 13, 46, 0, ZoneId.of("GMT+2")).toInstant();
|
||||
Duration duration = Duration.ofMinutes(5);
|
||||
|
||||
JSONBuilder cb = new JSONBuilder();
|
||||
cb.put("fooDate", date);
|
||||
cb.put("fooNull", null);
|
||||
cb.put("fooDuration", duration);
|
||||
cb.put("fooNull", (Object) null);
|
||||
|
||||
assertThat(cb.toString(), is("{\"fooDate\":\"2016-06-01T03:13:46Z\",\"fooNull\":null}"));
|
||||
assertThat(cb.toString(), is("{\"fooDate\":\"2016-06-01T03:13:46Z\",\"fooDuration\":300,\"fooNull\":null}"));
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -27,6 +27,7 @@ import java.io.ObjectOutputStream;
|
|||
import java.net.MalformedURLException;
|
||||
import java.net.URI;
|
||||
import java.net.URL;
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.time.LocalDate;
|
||||
import java.time.ZoneId;
|
||||
|
@ -91,7 +92,7 @@ public class JSONTest {
|
|||
|
||||
assertThat(json.keySet(), containsInAnyOrder(
|
||||
"text", "number", "boolean", "uri", "url", "date", "array",
|
||||
"collect", "status", "binary", "problem"));
|
||||
"collect", "status", "binary", "duration", "problem"));
|
||||
assertThat(json.contains("text"), is(true));
|
||||
assertThat(json.contains("music"), is(false));
|
||||
assertThat(json.get("text"), is(notNullValue()));
|
||||
|
@ -201,6 +202,7 @@ public class JSONTest {
|
|||
assertThat(json.get("date").asInstant(), is(date));
|
||||
assertThat(json.get("status").asStatus(), is(Status.VALID));
|
||||
assertThat(json.get("binary").asBinary(), is("Chainsaw".getBytes()));
|
||||
assertThat(json.get("duration").asDuration(), is(Duration.ofSeconds(86400)));
|
||||
|
||||
assertThat(json.get("text").isPresent(), is(true));
|
||||
assertThat(json.get("text").optional().isPresent(), is(true));
|
||||
|
@ -265,6 +267,13 @@ public class JSONTest {
|
|||
// expected
|
||||
}
|
||||
|
||||
try {
|
||||
json.get("none").asDuration();
|
||||
fail("asDuration did not fail");
|
||||
} catch (AcmeProtocolException ex) {
|
||||
// expected
|
||||
}
|
||||
|
||||
try {
|
||||
json.get("none").asObject();
|
||||
fail("asObject did not fail");
|
||||
|
@ -357,6 +366,13 @@ public class JSONTest {
|
|||
// expected
|
||||
}
|
||||
|
||||
try {
|
||||
json.get("text").asDuration();
|
||||
fail("no exception was thrown");
|
||||
} catch (AcmeProtocolException ex) {
|
||||
// expected
|
||||
}
|
||||
|
||||
try {
|
||||
json.get("text").asProblem(BASE_URL);
|
||||
fail("no exception was thrown");
|
||||
|
|
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"status": "canceled"
|
||||
}
|
|
@ -9,6 +9,7 @@
|
|||
"collect": ["foo", "bar", "barfoo"],
|
||||
"status": "VALID",
|
||||
"binary": "Q2hhaW5zYXc",
|
||||
"duration": 86400,
|
||||
"problem": {
|
||||
"type": "urn:ietf:params:acme:error:rateLimited",
|
||||
"detail": "too many requests",
|
||||
|
|
|
@ -10,6 +10,9 @@
|
|||
"example.com"
|
||||
],
|
||||
"externalAccountRequired": true,
|
||||
"star-enabled": true,
|
||||
"star-min-cert-validity": 86400,
|
||||
"star-max-renewal": 31536000,
|
||||
"xTestString": "foobar",
|
||||
"xTestUri": "https://www.example.org",
|
||||
"xTestArray": [
|
||||
|
|
|
@ -0,0 +1,12 @@
|
|||
{
|
||||
"identifiers": [
|
||||
{
|
||||
"type": "dns",
|
||||
"value": "example.org"
|
||||
}
|
||||
],
|
||||
"recurrent": true,
|
||||
"recurrent-start-date": "2018-01-01T00:00:00Z",
|
||||
"recurrent-end-date": "2019-01-01T00:00:00Z",
|
||||
"recurrent-certificate-validity": 604800
|
||||
}
|
|
@ -0,0 +1,19 @@
|
|||
{
|
||||
"status": "pending",
|
||||
"expires": "2016-01-10T00:00:00Z",
|
||||
"identifiers": [
|
||||
{
|
||||
"type": "dns",
|
||||
"value": "example.org"
|
||||
}
|
||||
],
|
||||
"recurrent": true,
|
||||
"recurrent-start-date": "2018-01-01T00:00:00Z",
|
||||
"recurrent-end-date": "2019-01-01T00:00:00Z",
|
||||
"recurrent-certificate-validity": 604800,
|
||||
"authorizations": [
|
||||
"https://example.com/acme/authz/1234",
|
||||
"https://example.com/acme/authz/2345"
|
||||
],
|
||||
"finalize": "https://example.com/acme/acct/1/order/1/finalize"
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
"status": "valid",
|
||||
"recurrent": true,
|
||||
"recurrent-start-date": "2016-01-01T00:00:00Z",
|
||||
"recurrent-end-date": "2017-01-01T00:00:00Z",
|
||||
"recurrent-certificate-validity": 604800
|
||||
}
|
|
@ -168,3 +168,33 @@ auth.deactivate();
|
|||
<div class="alert alert-info" role="alert">
|
||||
It is not documented if the deactivation of an authorization also revokes the related certificate. If the certificate should be revoked, revoke it manually before deactivation.
|
||||
</div>
|
||||
|
||||
## Short-Term Automatic Renewal
|
||||
|
||||
_acme4j_ supports the [ACME STAR](https://tools.ietf.org/html/draft-ietf-acme-star) extension for short-term automatic renewal of certificates.
|
||||
|
||||
To find out if the CA supports the STAR extension, check the metadata:
|
||||
|
||||
```java
|
||||
if (session.getMetadata().isStarEnabled()) {
|
||||
// CA supports STAR!
|
||||
}
|
||||
```
|
||||
|
||||
If STAR is supported, you can enable recurrent renewals by adding `recurrent()` to the order parameters:
|
||||
|
||||
```java
|
||||
Order order = account.newOrder()
|
||||
.domain("example.org")
|
||||
.recurrent()
|
||||
.create();
|
||||
```
|
||||
|
||||
You can use `recurrentStart()`, `recurrentEnd()` and `recurrentCertificateValidity()` to change the time span and frequency of automatic renewals. You cannot use `notBefore()` and `notAfter()` in combination with `recurrent()` though.
|
||||
|
||||
The `Metadata` object also holds the accepted renewal limits (see `Metadata.getStarMinCertValidity()` and `Metadata.getStarMaxRenewal()`).
|
||||
|
||||
<div class="alert alert-info" role="alert">
|
||||
|
||||
The _ACME STAR_ support is experimental. There is currently no known ACME server implementing this extension.
|
||||
</div>
|
||||
|
|
Loading…
Reference in New Issue