Support draft-ietf-acme-star-04

pull/81/head
Richard Körber 2019-01-12 18:11:38 +01:00
parent df0af217b3
commit 62d2e9c1c0
No known key found for this signature in database
GPG Key ID: AAB9FD19C78AA3E0
12 changed files with 159 additions and 2 deletions

View File

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

View File

@ -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.
* <p>
@ -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.
*

View File

@ -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.
* <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.
*
@ -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);

View File

@ -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();

View File

@ -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<Authorization> 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();
}

View File

@ -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()));
}

View File

@ -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": [

View File

@ -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"
}

View File

@ -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
}

View File

@ -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"

View File

@ -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
}

View File

@ -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.
<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.
</div>