From 0ccd68c09a28e23d6b20c61487800bb8f1cf36bf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Richard=20K=C3=B6rber?= Date: Sat, 24 Aug 2024 12:19:13 +0200 Subject: [PATCH] Update to draft-ietf-acme-ari-05 --- README.md | 2 +- .../org/shredzone/acme4j/OrderBuilder.java | 4 + .../shredzone/acme4j/OrderBuilderTest.java | 73 ++++++++++++++++++- .../json/requestAutoRenewOrderRequest.json | 3 +- .../json/requestReplacesRequest.json | 9 +++ .../json/requestReplacesResponse.json | 15 ++++ src/doc/docs/index.md | 2 +- src/doc/docs/usage/renewal.md | 2 +- 8 files changed, 102 insertions(+), 8 deletions(-) create mode 100644 acme4j-client/src/test/resources/json/requestReplacesRequest.json create mode 100644 acme4j-client/src/test/resources/json/requestReplacesResponse.json diff --git a/README.md b/README.md index 5c9232ad..b566841f 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,7 @@ This Java client helps connecting to an ACME server, and performing all necessar * Supports [RFC 8739](https://tools.ietf.org/html/rfc8739) short-term automatic certificate renewal (experimental) * Supports [RFC 8823](https://tools.ietf.org/html/rfc8823) for S/MIME certificates (experimental) * Supports [RFC 9444](https://tools.ietf.org/html/rfc9444) for subdomain validation -* Supports [draft-ietf-acme-ari-04](https://www.ietf.org/archive/id/draft-ietf-acme-ari-04.html) for renewal information (experimental) +* Supports [draft-ietf-acme-ari-05](https://www.ietf.org/archive/id/draft-ietf-acme-ari-05.html) for renewal information (experimental) * Easy to use Java API * Requires JRE 11 or higher * Supports [Let's Encrypt](https://letsencrypt.org/), [SSL.com](https://www.ssl.com/), [ZeroSSL](https://zerossl.com), and all other CAs that comply with the ACME protocol (RFC 8555). Note that _acme4j_ is an independent project that is not supported or endorsed by any of the CAs. 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 97e377fb..2996f635 100644 --- a/acme4j-client/src/main/java/org/shredzone/acme4j/OrderBuilder.java +++ b/acme4j-client/src/main/java/org/shredzone/acme4j/OrderBuilder.java @@ -343,6 +343,10 @@ public class OrderBuilder { throw new AcmeNotSupportedException("auto-renewal"); } + if (replaces != null && session.resourceUrlOptional(Resource.RENEWAL_INFO).isEmpty()) { + throw new AcmeNotSupportedException("renewal-information"); + } + var hasAncestorDomain = identifierSet.stream() .filter(id -> Identifier.TYPE_DNS.equals(id.getType())) .anyMatch(id -> id.toMap().containsKey(Identifier.KEY_ANCESTOR_DOMAIN)); 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 ed947a7a..ee7b9c51 100644 --- a/acme4j-client/src/test/java/org/shredzone/acme4j/OrderBuilderTest.java +++ b/acme4j-client/src/test/java/org/shredzone/acme4j/OrderBuilderTest.java @@ -14,8 +14,7 @@ package org.shredzone.acme4j; import static net.javacrumbs.jsonunit.assertj.JsonAssertions.assertThatJson; -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.assertj.core.api.Assertions.*; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.shredzone.acme4j.toolbox.AcmeUtils.parseTimestamp; import static org.shredzone.acme4j.toolbox.TestUtils.getJSON; @@ -169,7 +168,6 @@ public class OrderBuilderTest { .autoRenewalLifetime(validity) .autoRenewalLifetimeAdjust(predate) .autoRenewalEnableGet() - .replaces("aYhba4dGQEHhs3uEe6CuLN4ByNQ.AIdlQyE") .create(); try (var softly = new AutoCloseableSoftAssertions()) { @@ -335,4 +333,73 @@ public class OrderBuilderTest { provider.close(); } + /** + * Test that the ARI replaces field is set. + */ + @Test + public void testARIReplaces() throws Exception { + var provider = new TestableConnectionProvider() { + @Override + public int sendSignedRequest(URL url, JSONBuilder claims, Login login) { + assertThat(url).isEqualTo(resourceUrl); + assertThatJson(claims.toString()).isEqualTo(getJSON("requestReplacesRequest").toString()); + assertThat(login).isNotNull(); + return HttpURLConnection.HTTP_CREATED; + } + + @Override + public JSON readJsonResponse() { + return getJSON("requestReplacesResponse"); + } + + @Override + public URL getLocation() { + return locationUrl; + } + }; + + var login = provider.createLogin(); + + provider.putTestResource(Resource.NEW_ORDER, resourceUrl); + provider.putTestResource(Resource.RENEWAL_INFO, resourceUrl); + + var account = new Account(login); + account.newOrder() + .domain("example.org") + .replaces("aYhba4dGQEHhs3uEe6CuLN4ByNQ.AIdlQyE") + .create(); + + provider.close(); + } + + /** + * Test that exception is thrown if the ARI replaces field is set but ARI is not + * supported. + */ + @Test + public void testARIReplaceFails() throws Exception { + var provider = new TestableConnectionProvider() { + @Override + public int sendSignedRequest(URL url, JSONBuilder claims, Login login) { + fail("Request was sent"); + return HttpURLConnection.HTTP_FORBIDDEN; + } + }; + + var login = provider.createLogin(); + + provider.putTestResource(Resource.NEW_ORDER, resourceUrl); + + var account = new Account(login); + assertThatExceptionOfType(AcmeNotSupportedException.class).isThrownBy(() -> { + account.newOrder() + .domain("example.org") + .replaces("aYhba4dGQEHhs3uEe6CuLN4ByNQ.AIdlQyE") + .create(); + }) + .withMessage("Server does not support renewal-information"); + + provider.close(); + } + } diff --git a/acme4j-client/src/test/resources/json/requestAutoRenewOrderRequest.json b/acme4j-client/src/test/resources/json/requestAutoRenewOrderRequest.json index a1b7baa0..18a9b0fa 100644 --- a/acme4j-client/src/test/resources/json/requestAutoRenewOrderRequest.json +++ b/acme4j-client/src/test/resources/json/requestAutoRenewOrderRequest.json @@ -11,6 +11,5 @@ "lifetime": 604800, "lifetime-adjust": 518400, "allow-certificate-get": true - }, - "replaces": "aYhba4dGQEHhs3uEe6CuLN4ByNQ.AIdlQyE" + } } diff --git a/acme4j-client/src/test/resources/json/requestReplacesRequest.json b/acme4j-client/src/test/resources/json/requestReplacesRequest.json new file mode 100644 index 00000000..87bcb0b3 --- /dev/null +++ b/acme4j-client/src/test/resources/json/requestReplacesRequest.json @@ -0,0 +1,9 @@ +{ + "identifiers": [ + { + "type": "dns", + "value": "example.org" + } + ], + "replaces": "aYhba4dGQEHhs3uEe6CuLN4ByNQ.AIdlQyE" +} diff --git a/acme4j-client/src/test/resources/json/requestReplacesResponse.json b/acme4j-client/src/test/resources/json/requestReplacesResponse.json new file mode 100644 index 00000000..cd373c0a --- /dev/null +++ b/acme4j-client/src/test/resources/json/requestReplacesResponse.json @@ -0,0 +1,15 @@ +{ + "status": "pending", + "expires": "2016-01-10T00:00:00Z", + "identifiers": [ + { + "type": "dns", + "value": "example.org" + } + ], + "authorizations": [ + "https://example.com/acme/authz/1234", + "https://example.com/acme/authz/2345" + ], + "finalize": "https://example.com/acme/acct/1/order/1/finalize" +} diff --git a/src/doc/docs/index.md b/src/doc/docs/index.md index 1223728e..af7b64ff 100644 --- a/src/doc/docs/index.md +++ b/src/doc/docs/index.md @@ -19,7 +19,7 @@ Latest version: ![maven central](https://shredzone.org/maven-central/org.shredzo * Supports [RFC 8739](https://tools.ietf.org/html/rfc8739) short-term automatic certificate renewal (experimental) * Supports [RFC 8823](https://tools.ietf.org/html/rfc8823) for S/MIME certificates (experimental) * Supports [RFC 9444](https://tools.ietf.org/html/rfc9444) for subdomain validation -* Supports [draft-ietf-acme-ari-04](https://www.ietf.org/archive/id/draft-ietf-acme-ari-04.html) for renewal information (experimental) +* Supports [draft-ietf-acme-ari-05](https://www.ietf.org/archive/id/draft-ietf-acme-ari-05.html) for renewal information (experimental) * Easy to use Java API * Requires JRE 11 or higher * Supports [Let's Encrypt](https://letsencrypt.org/), [SSL.com](https://www.ssl.com/), [ZeroSSL](https://zerossl.com), and all other CAs that comply with the ACME protocol (RFC 8555). Note that _acme4j_ is an independent project that is not supported or endorsed by any of the CAs. diff --git a/src/doc/docs/usage/renewal.md b/src/doc/docs/usage/renewal.md index 0464de93..204e19f3 100644 --- a/src/doc/docs/usage/renewal.md +++ b/src/doc/docs/usage/renewal.md @@ -13,7 +13,7 @@ There is no special path for renewing a certificate. To renew it, just [order](o ## Renewal Information -_acme4j_ supports the [draft-ietf-acme-ari-04](https://www.ietf.org/archive/id/draft-ietf-acme-ari-04.html) draft. +_acme4j_ supports the [draft-ietf-acme-ari-05](https://www.ietf.org/archive/id/draft-ietf-acme-ari-05.html) draft. You can check if the CA offers renewal information by invoking `Certificate.hasRenewalInfo()`. If it does, you can get a suggested time window for certificate nenewal by invoking `Certificate.getRenewalInfo()`.