mirror of https://github.com/shred/acme4j
Support draft-ietf-acme-star-04
parent
df0af217b3
commit
62d2e9c1c0
|
@ -113,6 +113,15 @@ public class Metadata {
|
||||||
return meta.get("star-max-renewal").map(Value::asDuration).orElse(null);
|
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
|
* Returns the JSON representation of the metadata. This is useful for reading
|
||||||
* proprietary metadata properties.
|
* proprietary metadata properties.
|
||||||
|
|
|
@ -132,6 +132,20 @@ public class Order extends AcmeJsonResource {
|
||||||
.orElse(null);
|
.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.
|
* Finalizes the order, by providing a CSR.
|
||||||
* <p>
|
* <p>
|
||||||
|
@ -210,6 +224,19 @@ public class Order extends AcmeJsonResource {
|
||||||
.orElse(null);
|
.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.
|
* Cancels a recurrent order.
|
||||||
*
|
*
|
||||||
|
|
|
@ -50,6 +50,7 @@ public class OrderBuilder {
|
||||||
private Instant recurrentStart;
|
private Instant recurrentStart;
|
||||||
private Instant recurrentEnd;
|
private Instant recurrentEnd;
|
||||||
private Duration recurrentValidity;
|
private Duration recurrentValidity;
|
||||||
|
private boolean recurrentGet;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a new {@link OrderBuilder}.
|
* Create a new {@link OrderBuilder}.
|
||||||
|
@ -228,6 +229,26 @@ public class OrderBuilder {
|
||||||
return this;
|
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.
|
||||||
|
* <p>
|
||||||
|
* This option is only needed if you plan to fetch the STAR certificate via other
|
||||||
|
* means than by using acme4j.
|
||||||
|
* <p>
|
||||||
|
* 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.
|
* Sends a new order to the server, and returns an {@link Order} object.
|
||||||
*
|
*
|
||||||
|
@ -267,6 +288,9 @@ public class OrderBuilder {
|
||||||
if (recurrentValidity != null) {
|
if (recurrentValidity != null) {
|
||||||
claims.put("recurrent-certificate-validity", recurrentValidity);
|
claims.put("recurrent-certificate-validity", recurrentValidity);
|
||||||
}
|
}
|
||||||
|
if (recurrentGet) {
|
||||||
|
claims.put("recurrent-certificate-get", recurrentGet);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
conn.sendSignedRequest(session.resourceUrl(Resource.NEW_ORDER), claims, login);
|
conn.sendSignedRequest(session.resourceUrl(Resource.NEW_ORDER), claims, login);
|
||||||
|
|
|
@ -148,6 +148,7 @@ public class OrderBuilderTest {
|
||||||
.recurrentStart(recurrentStart)
|
.recurrentStart(recurrentStart)
|
||||||
.recurrentEnd(recurrentEnd)
|
.recurrentEnd(recurrentEnd)
|
||||||
.recurrentCertificateValidity(validity)
|
.recurrentCertificateValidity(validity)
|
||||||
|
.recurrentEnableGet()
|
||||||
.create();
|
.create();
|
||||||
|
|
||||||
assertThat(order.getIdentifiers(), containsInAnyOrder(Identifier.dns("example.org")));
|
assertThat(order.getIdentifiers(), containsInAnyOrder(Identifier.dns("example.org")));
|
||||||
|
@ -157,6 +158,7 @@ public class OrderBuilderTest {
|
||||||
assertThat(order.getRecurrentStart(), is(recurrentStart));
|
assertThat(order.getRecurrentStart(), is(recurrentStart));
|
||||||
assertThat(order.getRecurrentEnd(), is(recurrentEnd));
|
assertThat(order.getRecurrentEnd(), is(recurrentEnd));
|
||||||
assertThat(order.getRecurrentCertificateValidity(), is(validity));
|
assertThat(order.getRecurrentCertificateValidity(), is(validity));
|
||||||
|
assertThat(order.isRecurrentGetEnabled(), is(true));
|
||||||
assertThat(order.getLocation(), is(locationUrl));
|
assertThat(order.getLocation(), is(locationUrl));
|
||||||
|
|
||||||
provider.close();
|
provider.close();
|
||||||
|
|
|
@ -84,6 +84,7 @@ public class OrderTest {
|
||||||
assertThat(order.getRecurrentStart(), is(nullValue()));
|
assertThat(order.getRecurrentStart(), is(nullValue()));
|
||||||
assertThat(order.getRecurrentEnd(), is(nullValue()));
|
assertThat(order.getRecurrentEnd(), is(nullValue()));
|
||||||
assertThat(order.getRecurrentCertificateValidity(), is(nullValue()));
|
assertThat(order.getRecurrentCertificateValidity(), is(nullValue()));
|
||||||
|
assertThat(order.isRecurrentGetEnabled(), is(false));
|
||||||
|
|
||||||
assertThat(order.getError(), is(notNullValue()));
|
assertThat(order.getError(), is(notNullValue()));
|
||||||
assertThat(order.getError().getType(), is(URI.create("urn:ietf:params:acme:error:connection")));
|
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.getNotBefore(), is(parseTimestamp("2016-01-01T00:00:00Z")));
|
||||||
assertThat(order.getNotAfter(), is(parseTimestamp("2016-01-08T00: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.getCertificate().getLocation(), is(url("https://example.com/acme/cert/1234")));
|
||||||
|
assertThat(order.getStarCertificate(), is(nullValue()));
|
||||||
assertThat(order.getFinalizeLocation(), is(finalizeUrl));
|
assertThat(order.getFinalizeLocation(), is(finalizeUrl));
|
||||||
|
|
||||||
List<Authorization> auths = order.getAuthorizations();
|
List<Authorization> auths = order.getAuthorizations();
|
||||||
|
@ -243,6 +245,46 @@ public class OrderTest {
|
||||||
assertThat(order.getRecurrentCertificateValidity(), is(Duration.ofHours(168)));
|
assertThat(order.getRecurrentCertificateValidity(), is(Duration.ofHours(168)));
|
||||||
assertThat(order.getNotBefore(), is(nullValue()));
|
assertThat(order.getNotBefore(), is(nullValue()));
|
||||||
assertThat(order.getNotAfter(), 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();
|
provider.close();
|
||||||
}
|
}
|
||||||
|
|
|
@ -183,6 +183,7 @@ public class SessionTest {
|
||||||
assertThat(meta.isStarEnabled(), is(false));
|
assertThat(meta.isStarEnabled(), is(false));
|
||||||
assertThat(meta.getStarMaxRenewal(), is(nullValue()));
|
assertThat(meta.getStarMaxRenewal(), is(nullValue()));
|
||||||
assertThat(meta.getStarMinCertValidity(), 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.isStarEnabled(), is(true));
|
||||||
assertThat(meta.getStarMaxRenewal(), is(Duration.ofDays(365)));
|
assertThat(meta.getStarMaxRenewal(), is(Duration.ofDays(365)));
|
||||||
assertThat(meta.getStarMinCertValidity(), is(Duration.ofHours(24)));
|
assertThat(meta.getStarMinCertValidity(), is(Duration.ofHours(24)));
|
||||||
|
assertThat(meta.isStarCertificateGetAllowed(), is(true));
|
||||||
assertThat(meta.isExternalAccountRequired(), is(true));
|
assertThat(meta.isExternalAccountRequired(), is(true));
|
||||||
assertThat(meta.getJSON(), is(notNullValue()));
|
assertThat(meta.getJSON(), is(notNullValue()));
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,6 +13,7 @@
|
||||||
"star-enabled": true,
|
"star-enabled": true,
|
||||||
"star-min-cert-validity": 86400,
|
"star-min-cert-validity": 86400,
|
||||||
"star-max-renewal": 31536000,
|
"star-max-renewal": 31536000,
|
||||||
|
"star-allow-certificate-get": true,
|
||||||
"xTestString": "foobar",
|
"xTestString": "foobar",
|
||||||
"xTestUri": "https://www.example.org",
|
"xTestUri": "https://www.example.org",
|
||||||
"xTestArray": [
|
"xTestArray": [
|
||||||
|
|
|
@ -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"
|
||||||
|
}
|
|
@ -8,5 +8,6 @@
|
||||||
"recurrent": true,
|
"recurrent": true,
|
||||||
"recurrent-start-date": "2018-01-01T00:00:00Z",
|
"recurrent-start-date": "2018-01-01T00:00:00Z",
|
||||||
"recurrent-end-date": "2019-01-01T00:00:00Z",
|
"recurrent-end-date": "2019-01-01T00:00:00Z",
|
||||||
"recurrent-certificate-validity": 604800
|
"recurrent-certificate-validity": 604800,
|
||||||
|
"recurrent-certificate-get": true
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,6 +11,7 @@
|
||||||
"recurrent-start-date": "2018-01-01T00:00:00Z",
|
"recurrent-start-date": "2018-01-01T00:00:00Z",
|
||||||
"recurrent-end-date": "2019-01-01T00:00:00Z",
|
"recurrent-end-date": "2019-01-01T00:00:00Z",
|
||||||
"recurrent-certificate-validity": 604800,
|
"recurrent-certificate-validity": 604800,
|
||||||
|
"recurrent-certificate-get": true,
|
||||||
"authorizations": [
|
"authorizations": [
|
||||||
"https://example.com/acme/authz/1234",
|
"https://example.com/acme/authz/1234",
|
||||||
"https://example.com/acme/authz/2345"
|
"https://example.com/acme/authz/2345"
|
||||||
|
|
|
@ -3,5 +3,6 @@
|
||||||
"recurrent": true,
|
"recurrent": true,
|
||||||
"recurrent-start-date": "2016-01-01T00:00:00Z",
|
"recurrent-start-date": "2016-01-01T00:00:00Z",
|
||||||
"recurrent-end-date": "2017-01-01T00:00:00Z",
|
"recurrent-end-date": "2017-01-01T00:00:00Z",
|
||||||
"recurrent-certificate-validity": 604800
|
"recurrent-certificate-validity": 604800,
|
||||||
|
"recurrent-certificate-get": true
|
||||||
}
|
}
|
||||||
|
|
|
@ -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()`).
|
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.
|
||||||
|
|
||||||
<div class="alert alert-info" role="alert">
|
<div class="alert alert-info" role="alert">
|
||||||
|
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.
|
||||||
|
</div>
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
<div class="alert alert-warning" role="alert">
|
||||||
|
|
||||||
The _ACME STAR_ support is experimental. There is currently no known ACME server implementing this extension.
|
The _ACME STAR_ support is experimental. There is currently no known ACME server implementing this extension.
|
||||||
</div>
|
</div>
|
||||||
|
|
Loading…
Reference in New Issue