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. +

+ +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. + +