diff --git a/acme4j-client/src/main/java/org/shredzone/acme4j/Metadata.java b/acme4j-client/src/main/java/org/shredzone/acme4j/Metadata.java
index 7ef78b77..ffeedfcc 100644
--- a/acme4j-client/src/main/java/org/shredzone/acme4j/Metadata.java
+++ b/acme4j-client/src/main/java/org/shredzone/acme4j/Metadata.java
@@ -113,6 +113,15 @@ public class Metadata {
return meta.get("star-max-renewal").map(Value::asDuration).orElse(null);
}
+ /**
+ * Returns whether the CA also allows to fetch STAR certificates via GET request.
+ *
+ * @since 2.6
+ */
+ public boolean isStarCertificateGetAllowed() {
+ return meta.get("star-allow-certificate-get").map(Value::asBoolean).orElse(false);
+ }
+
/**
* Returns the JSON representation of the metadata. This is useful for reading
* proprietary metadata properties.
diff --git a/acme4j-client/src/main/java/org/shredzone/acme4j/Order.java b/acme4j-client/src/main/java/org/shredzone/acme4j/Order.java
index d75d99e5..00adb7d7 100644
--- a/acme4j-client/src/main/java/org/shredzone/acme4j/Order.java
+++ b/acme4j-client/src/main/java/org/shredzone/acme4j/Order.java
@@ -132,6 +132,20 @@ public class Order extends AcmeJsonResource {
.orElse(null);
}
+ /**
+ * Gets the STAR extension's {@link Certificate} if it is available. {@code null}
+ * otherwise.
+ *
+ * @since 2.6
+ */
+ @CheckForNull
+ public Certificate getStarCertificate() {
+ return getJSON().get("star-certificate")
+ .map(Value::asURL)
+ .map(getLogin()::bindCertificate)
+ .orElse(null);
+ }
+
/**
* Finalizes the order, by providing a CSR.
*
@@ -210,6 +224,19 @@ public class Order extends AcmeJsonResource {
.orElse(null);
}
+ /**
+ * Returns {@code true} if STAR certificates from this order can also be fetched via
+ * GET requests.
+ *
+ * @since 2.6
+ */
+ public boolean isRecurrentGetEnabled() {
+ return getJSON().get("recurrent-certificate-get")
+ .optional()
+ .map(Value::asBoolean)
+ .orElse(false);
+ }
+
/**
* Cancels a recurrent order.
*
diff --git a/acme4j-client/src/main/java/org/shredzone/acme4j/OrderBuilder.java b/acme4j-client/src/main/java/org/shredzone/acme4j/OrderBuilder.java
index bc6997f0..13796554 100644
--- a/acme4j-client/src/main/java/org/shredzone/acme4j/OrderBuilder.java
+++ b/acme4j-client/src/main/java/org/shredzone/acme4j/OrderBuilder.java
@@ -50,6 +50,7 @@ public class OrderBuilder {
private Instant recurrentStart;
private Instant recurrentEnd;
private Duration recurrentValidity;
+ private boolean recurrentGet;
/**
* Create a new {@link OrderBuilder}.
@@ -228,6 +229,26 @@ public class OrderBuilder {
return this;
}
+ /**
+ * Announces that the client wishes to fetch the recurring certificate via GET
+ * request. If not used, the STAR certificate can only be fetched via POST-as-GET
+ * request. {@link Metadata#isStarCertificateGetAllowed()} must return {@code true} in
+ * order for this option to work.
+ *
+ * This option is only needed if you plan to fetch the STAR certificate via other
+ * means than by using acme4j.
+ *
+ * Implies {@link #recurrent()}.
+ *
+ * @return itself
+ * @since 2.6
+ */
+ public OrderBuilder recurrentEnableGet() {
+ recurrent();
+ this.recurrentGet = true;
+ return this;
+ }
+
/**
* Sends a new order to the server, and returns an {@link Order} object.
*
@@ -267,6 +288,9 @@ public class OrderBuilder {
if (recurrentValidity != null) {
claims.put("recurrent-certificate-validity", recurrentValidity);
}
+ if (recurrentGet) {
+ claims.put("recurrent-certificate-get", recurrentGet);
+ }
}
conn.sendSignedRequest(session.resourceUrl(Resource.NEW_ORDER), claims, login);
diff --git a/acme4j-client/src/test/java/org/shredzone/acme4j/OrderBuilderTest.java b/acme4j-client/src/test/java/org/shredzone/acme4j/OrderBuilderTest.java
index 06163955..d3514fb0 100644
--- a/acme4j-client/src/test/java/org/shredzone/acme4j/OrderBuilderTest.java
+++ b/acme4j-client/src/test/java/org/shredzone/acme4j/OrderBuilderTest.java
@@ -148,6 +148,7 @@ public class OrderBuilderTest {
.recurrentStart(recurrentStart)
.recurrentEnd(recurrentEnd)
.recurrentCertificateValidity(validity)
+ .recurrentEnableGet()
.create();
assertThat(order.getIdentifiers(), containsInAnyOrder(Identifier.dns("example.org")));
@@ -157,6 +158,7 @@ public class OrderBuilderTest {
assertThat(order.getRecurrentStart(), is(recurrentStart));
assertThat(order.getRecurrentEnd(), is(recurrentEnd));
assertThat(order.getRecurrentCertificateValidity(), is(validity));
+ assertThat(order.isRecurrentGetEnabled(), is(true));
assertThat(order.getLocation(), is(locationUrl));
provider.close();
diff --git a/acme4j-client/src/test/java/org/shredzone/acme4j/OrderTest.java b/acme4j-client/src/test/java/org/shredzone/acme4j/OrderTest.java
index e601331b..fbb26a94 100644
--- a/acme4j-client/src/test/java/org/shredzone/acme4j/OrderTest.java
+++ b/acme4j-client/src/test/java/org/shredzone/acme4j/OrderTest.java
@@ -84,6 +84,7 @@ public class OrderTest {
assertThat(order.getRecurrentStart(), is(nullValue()));
assertThat(order.getRecurrentEnd(), is(nullValue()));
assertThat(order.getRecurrentCertificateValidity(), is(nullValue()));
+ assertThat(order.isRecurrentGetEnabled(), is(false));
assertThat(order.getError(), is(notNullValue()));
assertThat(order.getError().getType(), is(URI.create("urn:ietf:params:acme:error:connection")));
@@ -195,6 +196,7 @@ public class OrderTest {
assertThat(order.getNotBefore(), is(parseTimestamp("2016-01-01T00:00:00Z")));
assertThat(order.getNotAfter(), is(parseTimestamp("2016-01-08T00:00:00Z")));
assertThat(order.getCertificate().getLocation(), is(url("https://example.com/acme/cert/1234")));
+ assertThat(order.getStarCertificate(), is(nullValue()));
assertThat(order.getFinalizeLocation(), is(finalizeUrl));
List auths = order.getAuthorizations();
@@ -243,6 +245,46 @@ public class OrderTest {
assertThat(order.getRecurrentCertificateValidity(), is(Duration.ofHours(168)));
assertThat(order.getNotBefore(), is(nullValue()));
assertThat(order.getNotAfter(), is(nullValue()));
+ assertThat(order.isRecurrentGetEnabled(), is(true));
+
+ provider.close();
+ }
+
+ /**
+ * Test that recurrent order is properly finalized.
+ */
+ @Test
+ public void testRecurrentFinalize() throws Exception {
+ TestableConnectionProvider provider = new TestableConnectionProvider() {
+ @Override
+ public int sendSignedPostAsGetRequest(URL url, Login login) {
+ assertThat(url, is(locationUrl));
+ return HttpURLConnection.HTTP_OK;
+ }
+
+ @Override
+ public JSON readJsonResponse() {
+ return getJSON("finalizeRecurrentResponse");
+ }
+
+ @Override
+ public void handleRetryAfter(String message) {
+ assertThat(message, not(nullValue()));
+ }
+ };
+
+ Login login = provider.createLogin();
+ Order order = login.bindOrder(locationUrl);
+
+ assertThat(order.getCertificate(), is(nullValue()));
+ assertThat(order.getStarCertificate().getLocation(), is(url("https://example.com/acme/cert/1234")));
+ assertThat(order.isRecurrent(), is(true));
+ assertThat(order.getRecurrentStart(), is(parseTimestamp("2018-01-01T00:00:00Z")));
+ assertThat(order.getRecurrentEnd(), is(parseTimestamp("2019-01-01T00:00:00Z")));
+ assertThat(order.getRecurrentCertificateValidity(), is(Duration.ofHours(168)));
+ assertThat(order.getNotBefore(), is(nullValue()));
+ assertThat(order.getNotAfter(), is(nullValue()));
+ assertThat(order.isRecurrentGetEnabled(), is(true));
provider.close();
}
diff --git a/acme4j-client/src/test/java/org/shredzone/acme4j/SessionTest.java b/acme4j-client/src/test/java/org/shredzone/acme4j/SessionTest.java
index 9d17e5cc..94dc260b 100644
--- a/acme4j-client/src/test/java/org/shredzone/acme4j/SessionTest.java
+++ b/acme4j-client/src/test/java/org/shredzone/acme4j/SessionTest.java
@@ -183,6 +183,7 @@ public class SessionTest {
assertThat(meta.isStarEnabled(), is(false));
assertThat(meta.getStarMaxRenewal(), is(nullValue()));
assertThat(meta.getStarMinCertValidity(), is(nullValue()));
+ assertThat(meta.isStarCertificateGetAllowed(), is(false));
}
/**
@@ -215,6 +216,7 @@ public class SessionTest {
assertThat(meta.isStarEnabled(), is(true));
assertThat(meta.getStarMaxRenewal(), is(Duration.ofDays(365)));
assertThat(meta.getStarMinCertValidity(), is(Duration.ofHours(24)));
+ assertThat(meta.isStarCertificateGetAllowed(), is(true));
assertThat(meta.isExternalAccountRequired(), is(true));
assertThat(meta.getJSON(), is(notNullValue()));
}
diff --git a/acme4j-client/src/test/resources/json/directory.json b/acme4j-client/src/test/resources/json/directory.json
index 850158c6..4a887e00 100644
--- a/acme4j-client/src/test/resources/json/directory.json
+++ b/acme4j-client/src/test/resources/json/directory.json
@@ -13,6 +13,7 @@
"star-enabled": true,
"star-min-cert-validity": 86400,
"star-max-renewal": 31536000,
+ "star-allow-certificate-get": true,
"xTestString": "foobar",
"xTestUri": "https://www.example.org",
"xTestArray": [
diff --git a/acme4j-client/src/test/resources/json/finalizeRecurrentResponse.json b/acme4j-client/src/test/resources/json/finalizeRecurrentResponse.json
new file mode 100644
index 00000000..f4698875
--- /dev/null
+++ b/acme4j-client/src/test/resources/json/finalizeRecurrentResponse.json
@@ -0,0 +1,25 @@
+{
+ "status": "valid",
+ "expires": "2015-03-01T14:09:00Z",
+ "identifiers": [
+ {
+ "type": "dns",
+ "value": "example.com"
+ },
+ {
+ "type": "dns",
+ "value": "www.example.com"
+ }
+ ],
+ "recurrent": true,
+ "recurrent-start-date": "2018-01-01T00:00:00Z",
+ "recurrent-end-date": "2019-01-01T00:00:00Z",
+ "recurrent-certificate-validity": 604800,
+ "recurrent-certificate-get": true,
+ "authorizations": [
+ "https://example.com/acme/authz/1234",
+ "https://example.com/acme/authz/2345"
+ ],
+ "finalize": "https://example.com/acme/acct/1/order/1/finalize",
+ "star-certificate": "https://example.com/acme/cert/1234"
+}
diff --git a/acme4j-client/src/test/resources/json/requestRecurrentOrderRequest.json b/acme4j-client/src/test/resources/json/requestRecurrentOrderRequest.json
index 53b99ec0..1aeb5c75 100644
--- a/acme4j-client/src/test/resources/json/requestRecurrentOrderRequest.json
+++ b/acme4j-client/src/test/resources/json/requestRecurrentOrderRequest.json
@@ -8,5 +8,6 @@
"recurrent": true,
"recurrent-start-date": "2018-01-01T00:00:00Z",
"recurrent-end-date": "2019-01-01T00:00:00Z",
- "recurrent-certificate-validity": 604800
+ "recurrent-certificate-validity": 604800,
+ "recurrent-certificate-get": true
}
diff --git a/acme4j-client/src/test/resources/json/requestRecurrentOrderResponse.json b/acme4j-client/src/test/resources/json/requestRecurrentOrderResponse.json
index bf1d33ba..60f5fc7e 100644
--- a/acme4j-client/src/test/resources/json/requestRecurrentOrderResponse.json
+++ b/acme4j-client/src/test/resources/json/requestRecurrentOrderResponse.json
@@ -11,6 +11,7 @@
"recurrent-start-date": "2018-01-01T00:00:00Z",
"recurrent-end-date": "2019-01-01T00:00:00Z",
"recurrent-certificate-validity": 604800,
+ "recurrent-certificate-get": true,
"authorizations": [
"https://example.com/acme/authz/1234",
"https://example.com/acme/authz/2345"
diff --git a/acme4j-client/src/test/resources/json/updateRecurrentOrderResponse.json b/acme4j-client/src/test/resources/json/updateRecurrentOrderResponse.json
index ebcfd9eb..7386f337 100644
--- a/acme4j-client/src/test/resources/json/updateRecurrentOrderResponse.json
+++ b/acme4j-client/src/test/resources/json/updateRecurrentOrderResponse.json
@@ -3,5 +3,6 @@
"recurrent": true,
"recurrent-start-date": "2016-01-01T00:00:00Z",
"recurrent-end-date": "2017-01-01T00:00:00Z",
- "recurrent-certificate-validity": 604800
+ "recurrent-certificate-validity": 604800,
+ "recurrent-certificate-get": true
}
diff --git a/src/site/markdown/usage/order.md b/src/site/markdown/usage/order.md
index 491bb667..2b009e00 100644
--- a/src/site/markdown/usage/order.md
+++ b/src/site/markdown/usage/order.md
@@ -218,7 +218,29 @@ You can use `recurrentStart()`, `recurrentEnd()` and `recurrentCertificateValidi
The `Metadata` object also holds the accepted renewal limits (see `Metadata.getStarMinCertValidity()` and `Metadata.getStarMaxRenewal()`).
+After the validation process is completed and the order is finalized, the STAR certificate is available via `Order.getStarCertificate()` (_not_ `Order.getCertificate()`)!
+
+Use `Certificate.getLocation()` to retrieve the URL of your certificate. It is renewed automatically, so you will always be able to download the latest issue of the certificate from this URL.
+
+STAR based certificates cannot be revoked. However, as it is the nature of these certs to be short-lived, this does not pose an actual security issue.
+
+
+To download the latest certificate issue, you can bind the certificate URL to your `Login` and then use the `Certificate` object.
+
+```java
+URL certificateUrl = ... // URL of the certificate
+
+Certificate cert = login.bindCertificate(certificateUrl);
+X509Certificate latestCertificate = cert.getCertificate();
+
+```
+
+If supported by the CA, it is possible to negotiate that the certificate can also be downloaded via `GET` request. First use `Metadata.isStarCertificateGetAllowed()` to check if this option is supported by the CA. If it is, add `recurrentEnableGet()` to the order parameters to enable it. After the order was finalized, you can use any HTTP client to download the latest certificate from the certificate URL by a `GET` request.
+
+Use `Order.cancelRecurrent()` to terminate automatical certificate renewals.
+
+
The _ACME STAR_ support is experimental. There is currently no known ACME server implementing this extension.