From bb91000fb27bac5d642dcabb18ffc85449b5e54e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Richard=20K=C3=B6rber?= Date: Tue, 13 Mar 2018 22:13:46 +0100 Subject: [PATCH 1/6] Wait for the order to become valid --- .../java/org/shredzone/acme4j/ClientTest.java | 24 ++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/acme4j-example/src/main/java/org/shredzone/acme4j/ClientTest.java b/acme4j-example/src/main/java/org/shredzone/acme4j/ClientTest.java index 16d82f69..69e2d501 100644 --- a/acme4j-example/src/main/java/org/shredzone/acme4j/ClientTest.java +++ b/acme4j-example/src/main/java/org/shredzone/acme4j/ClientTest.java @@ -104,8 +104,30 @@ public class ClientTest { csrb.write(out); } - // Get the certificate + // Order the certificate order.execute(csrb.getEncoded()); + + // Wait for the order to complete + try { + int attempts = 10; + while (order.getStatus() != Status.VALID && attempts-- > 0) { + // Did the order fail? + if (order.getStatus() == Status.INVALID) { + throw new AcmeException("Order failed... Giving up."); + } + + // Wait for a few seconds + Thread.sleep(3000L); + + // Then update the status + order.update(); + } + } catch (InterruptedException ex) { + LOG.error("interrupted", ex); + Thread.currentThread().interrupt(); + } + + // Get the certificate Certificate certificate = order.getCertificate(); LOG.info("Success! The certificate for domains " + domains + " has been generated!"); From 7cfcbc99b67093b858312da2d24ed63f759a8398 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Richard=20K=C3=B6rber?= Date: Tue, 13 Mar 2018 22:14:05 +0100 Subject: [PATCH 2/6] Handle empty JSON responses --- .../main/java/org/shredzone/acme4j/Account.java | 15 ++++++++++++--- .../java/org/shredzone/acme4j/AccountBuilder.java | 6 +++++- .../java/org/shredzone/acme4j/Authorization.java | 5 ++++- .../java/org/shredzone/acme4j/OrderBuilder.java | 6 +++++- .../org/shredzone/acme4j/challenge/Challenge.java | 5 ++++- .../shredzone/acme4j/connector/Connection.java | 2 +- .../acme4j/connector/DefaultConnection.java | 4 ++++ .../acme4j/connector/DefaultConnectionTest.java | 8 ++++++++ 8 files changed, 43 insertions(+), 8 deletions(-) diff --git a/acme4j-client/src/main/java/org/shredzone/acme4j/Account.java b/acme4j-client/src/main/java/org/shredzone/acme4j/Account.java index 5df7326d..fe1e3ede 100644 --- a/acme4j-client/src/main/java/org/shredzone/acme4j/Account.java +++ b/acme4j-client/src/main/java/org/shredzone/acme4j/Account.java @@ -158,7 +158,10 @@ public class Account extends AcmeJsonResource { conn.sendSignedRequest(newAuthzUrl, claims, getLogin()); Authorization auth = getLogin().bindAuthorization(conn.getLocation()); - auth.setJSON(conn.readJsonResponse()); + JSON json = conn.readJsonResponse(); + if (json != null) { + auth.setJSON(json); + } return auth; } } @@ -224,7 +227,10 @@ public class Account extends AcmeJsonResource { conn.sendSignedRequest(getLocation(), claims, getLogin()); - setJSON(conn.readJsonResponse()); + JSON json = conn.readJsonResponse(); + if (json != null) { + setJSON(json); + } } } @@ -294,7 +300,10 @@ public class Account extends AcmeJsonResource { conn.sendSignedRequest(getLocation(), claims, getLogin()); - setJSON(conn.readJsonResponse()); + JSON json = conn.readJsonResponse(); + if (json != null) { + setJSON(json); + } } } } diff --git a/acme4j-client/src/main/java/org/shredzone/acme4j/AccountBuilder.java b/acme4j-client/src/main/java/org/shredzone/acme4j/AccountBuilder.java index 23a48964..09330fc9 100644 --- a/acme4j-client/src/main/java/org/shredzone/acme4j/AccountBuilder.java +++ b/acme4j-client/src/main/java/org/shredzone/acme4j/AccountBuilder.java @@ -35,6 +35,7 @@ import org.shredzone.acme4j.connector.Connection; import org.shredzone.acme4j.connector.Resource; import org.shredzone.acme4j.exception.AcmeException; import org.shredzone.acme4j.toolbox.AcmeUtils; +import org.shredzone.acme4j.toolbox.JSON; import org.shredzone.acme4j.toolbox.JSONBuilder; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -210,7 +211,10 @@ public class AccountBuilder { URL location = conn.getLocation(); Login login = new Login(location, keyPair, session); - login.getAccount().setJSON(conn.readJsonResponse()); + JSON json = conn.readJsonResponse(); + if (json != null) { + login.getAccount().setJSON(json); + } return login; } } diff --git a/acme4j-client/src/main/java/org/shredzone/acme4j/Authorization.java b/acme4j-client/src/main/java/org/shredzone/acme4j/Authorization.java index c19c2f47..58010aa1 100644 --- a/acme4j-client/src/main/java/org/shredzone/acme4j/Authorization.java +++ b/acme4j-client/src/main/java/org/shredzone/acme4j/Authorization.java @@ -132,7 +132,10 @@ public class Authorization extends AcmeJsonResource { conn.sendSignedRequest(getLocation(), claims, getLogin()); - setJSON(conn.readJsonResponse()); + JSON json = conn.readJsonResponse(); + if (json != null) { + setJSON(json); + } } } 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 c882a26a..27befdb9 100644 --- a/acme4j-client/src/main/java/org/shredzone/acme4j/OrderBuilder.java +++ b/acme4j-client/src/main/java/org/shredzone/acme4j/OrderBuilder.java @@ -25,6 +25,7 @@ import java.util.Set; import org.shredzone.acme4j.connector.Connection; import org.shredzone.acme4j.connector.Resource; import org.shredzone.acme4j.exception.AcmeException; +import org.shredzone.acme4j.toolbox.JSON; import org.shredzone.acme4j.toolbox.JSONBuilder; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -152,7 +153,10 @@ public class OrderBuilder { conn.sendSignedRequest(session.resourceUrl(Resource.NEW_ORDER), claims, login); Order order = new Order(login, conn.getLocation()); - order.setJSON(conn.readJsonResponse()); + JSON json = conn.readJsonResponse(); + if (json != null) { + order.setJSON(json); + } return order; } } diff --git a/acme4j-client/src/main/java/org/shredzone/acme4j/challenge/Challenge.java b/acme4j-client/src/main/java/org/shredzone/acme4j/challenge/Challenge.java index b59ddc36..a2787513 100644 --- a/acme4j-client/src/main/java/org/shredzone/acme4j/challenge/Challenge.java +++ b/acme4j-client/src/main/java/org/shredzone/acme4j/challenge/Challenge.java @@ -145,7 +145,10 @@ public class Challenge extends AcmeJsonResource { conn.sendSignedRequest(getLocation(), claims, getLogin()); - setJSON(conn.readJsonResponse()); + JSON json = conn.readJsonResponse(); + if (json != null) { + setJSON(json); + } } } diff --git a/acme4j-client/src/main/java/org/shredzone/acme4j/connector/Connection.java b/acme4j-client/src/main/java/org/shredzone/acme4j/connector/Connection.java index 056f10a2..d0c52323 100644 --- a/acme4j-client/src/main/java/org/shredzone/acme4j/connector/Connection.java +++ b/acme4j-client/src/main/java/org/shredzone/acme4j/connector/Connection.java @@ -94,7 +94,7 @@ public interface Connection extends AutoCloseable { /** * Reads a server response as JSON data. * - * @return The JSON response + * @return The JSON response, or {@code null} if the server did not provide any data. */ JSON readJsonResponse() throws AcmeException; diff --git a/acme4j-client/src/main/java/org/shredzone/acme4j/connector/DefaultConnection.java b/acme4j-client/src/main/java/org/shredzone/acme4j/connector/DefaultConnection.java index a2a910fd..430fb020 100644 --- a/acme4j-client/src/main/java/org/shredzone/acme4j/connector/DefaultConnection.java +++ b/acme4j-client/src/main/java/org/shredzone/acme4j/connector/DefaultConnection.java @@ -192,6 +192,10 @@ public class DefaultConnection implements Connection { public JSON readJsonResponse() throws AcmeException { assertConnectionIsOpen(); + if (conn.getContentLength() == 0) { + return null; + } + String contentType = AcmeUtils.getContentType(conn.getHeaderField(CONTENT_TYPE_HEADER)); if (!("application/json".equals(contentType) || "application/problem+json".equals(contentType))) { diff --git a/acme4j-client/src/test/java/org/shredzone/acme4j/connector/DefaultConnectionTest.java b/acme4j-client/src/test/java/org/shredzone/acme4j/connector/DefaultConnectionTest.java index 52238cd9..da1c71ab 100644 --- a/acme4j-client/src/test/java/org/shredzone/acme4j/connector/DefaultConnectionTest.java +++ b/acme4j-client/src/test/java/org/shredzone/acme4j/connector/DefaultConnectionTest.java @@ -427,6 +427,7 @@ public class DefaultConnectionTest { String jsonData = "{\"type\":\"urn:ietf:params:acme:error:unauthorized\",\"detail\":\"Invalid response: 404\"}"; when(mockUrlConnection.getHeaderField("Content-Type")).thenReturn("application/problem+json"); + when(mockUrlConnection.getContentLength()).thenReturn(jsonData.length()); when(mockUrlConnection.getResponseCode()).thenReturn(HttpURLConnection.HTTP_FORBIDDEN); when(mockUrlConnection.getOutputStream()).thenReturn(new ByteArrayOutputStream()); when(mockUrlConnection.getErrorStream()).thenReturn(new ByteArrayInputStream(jsonData.getBytes("utf-8"))); @@ -446,6 +447,7 @@ public class DefaultConnectionTest { verify(mockUrlConnection, atLeastOnce()).getHeaderField("Content-Type"); verify(mockUrlConnection, atLeastOnce()).getResponseCode(); + verify(mockUrlConnection).getContentLength(); verify(mockUrlConnection).getErrorStream(); verify(mockUrlConnection).getURL(); } @@ -461,6 +463,7 @@ public class DefaultConnectionTest { linkHeader.put("Link", Arrays.asList("; rel=\"terms-of-service\"")); when(mockUrlConnection.getHeaderField("Content-Type")).thenReturn("application/problem+json"); + when(mockUrlConnection.getContentLength()).thenReturn(jsonData.length()); when(mockUrlConnection.getHeaderFields()).thenReturn(linkHeader); when(mockUrlConnection.getResponseCode()).thenReturn(HttpURLConnection.HTTP_FORBIDDEN); when(mockUrlConnection.getOutputStream()).thenReturn(new ByteArrayOutputStream()); @@ -484,6 +487,7 @@ public class DefaultConnectionTest { verify(mockUrlConnection, atLeastOnce()).getHeaderFields(); verify(mockUrlConnection, atLeastOnce()).getResponseCode(); verify(mockUrlConnection).getErrorStream(); + verify(mockUrlConnection).getContentLength(); verify(mockUrlConnection, atLeastOnce()).getURL(); } @@ -500,6 +504,7 @@ public class DefaultConnectionTest { Instant retryAfter = Instant.now().plusSeconds(30L).truncatedTo(ChronoUnit.MILLIS); when(mockUrlConnection.getHeaderField("Content-Type")).thenReturn("application/problem+json"); + when(mockUrlConnection.getContentLength()).thenReturn(jsonData.length()); when(mockUrlConnection.getHeaderField("Retry-After")).thenReturn(retryAfter.toString()); when(mockUrlConnection.getHeaderFieldDate("Retry-After", 0L)).thenReturn(retryAfter.toEpochMilli()); when(mockUrlConnection.getHeaderFields()).thenReturn(linkHeader); @@ -529,6 +534,7 @@ public class DefaultConnectionTest { verify(mockUrlConnection).getHeaderFieldDate("Retry-After", 0L); verify(mockUrlConnection, atLeastOnce()).getHeaderFields(); verify(mockUrlConnection, atLeastOnce()).getResponseCode(); + verify(mockUrlConnection).getContentLength(); verify(mockUrlConnection).getErrorStream(); verify(mockUrlConnection, atLeastOnce()).getURL(); } @@ -833,6 +839,7 @@ public class DefaultConnectionTest { String jsonData = "{\n\"foo\":123,\n\"bar\":\"a-string\"\n}\n"; when(mockUrlConnection.getHeaderField("Content-Type")).thenReturn("application/json"); + when(mockUrlConnection.getContentLength()).thenReturn(jsonData.length()); when(mockUrlConnection.getResponseCode()).thenReturn(HttpURLConnection.HTTP_OK); when(mockUrlConnection.getInputStream()).thenReturn(new ByteArrayInputStream(jsonData.getBytes("utf-8"))); @@ -845,6 +852,7 @@ public class DefaultConnectionTest { } verify(mockUrlConnection).getHeaderField("Content-Type"); + verify(mockUrlConnection).getContentLength(); verify(mockUrlConnection).getResponseCode(); verify(mockUrlConnection).getInputStream(); verifyNoMoreInteractions(mockUrlConnection); From a7d65026630d22003142f1820a3a2a69cae73200 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Richard=20K=C3=B6rber?= Date: Tue, 13 Mar 2018 22:57:59 +0100 Subject: [PATCH 3/6] Add help for the 'Malformed account ID in KeyID header' error --- src/site/markdown/migration.md | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/src/site/markdown/migration.md b/src/site/markdown/migration.md index a7c0a3b6..f1b15e01 100644 --- a/src/site/markdown/migration.md +++ b/src/site/markdown/migration.md @@ -8,9 +8,6 @@ _acme4j_ 2.0 fully supports the ACMEv2 protocol. Sadly, the ACMEv2 protocol is a There is no easy recipe to migrate your code to _acme4j_ 2.0. I recommend to have a look at the example, and read this documentation. Altogether, it shouldn't be too much work to update your code, though. -## Migration from Version 2.0-SNAPSHOT (GitHub master branch) +### "Malformed account ID in KeyID header" -* The `Session` object has been split into `Session` and `Login`. The `Session` now only tracks the communication, and does not need an account key pair any more. -* To get a `Login` to an existing `Account`, use `Session.login()` with the account key pair and account URL. -* If you create a new `Account` using the `AccountBuilder`, you must pass in the key pair via `AccountBuilder.useKeyPair()`. -* You can find all resource bind methods in `Login` now. +If you try to use your old ACME v1 account location URL in ACME v2, you will get a "Malformed account ID in KeyID header" error. The easiest way to fix this is to register a new account **with your existing account key pair**. You will get your migrated account location URL in return. From e02e319a117b7ab859ea1f5e3fb84c13221d73c6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Richard=20K=C3=B6rber?= Date: Tue, 13 Mar 2018 23:13:47 +0100 Subject: [PATCH 4/6] Review README --- README.md | 21 ++++----------------- 1 file changed, 4 insertions(+), 17 deletions(-) diff --git a/README.md b/README.md index 68bc5ad3..33768dfe 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,5 @@ # ACME Java Client ![build status](https://shredzone.org/badge/acme4j.svg) ![maven central](https://maven-badges.herokuapp.com/maven-central/org.shredzone.acme4j/acme4j/badge.svg) -> **NOTE:** There is currently no _acme4j_ 2.0 release available at Maven Central. To use _acme4j_ with the ACMEv2 protocol, you need to build it yourself and use version `2.0-SNAPSHOT` in your project. Version 2.0 will be available as soon as _Let's Encrypt_ starts its production ACMEv2 server. -> -> **For production** you should use the latest version available at Maven Central (see the badge above). You can find the corresponding source code in the [acmev1 branch](https://github.com/shred/acme4j/tree/acmev1). - This is a Java client for the [Automatic Certificate Management Environment (ACME)](https://tools.ietf.org/html/draft-ietf-acme-acme-09) protocol. ACME is a protocol that a certificate authority (CA) and an applicant can use to automate the process of verification and certificate issuance. @@ -21,26 +17,17 @@ It is an independent open source implementation that is not affiliated with or e * Small, only requires [jose4j](https://bitbucket.org/b_c/jose4j/wiki/Home) and [slf4j](http://www.slf4j.org/) as dependencies * Extensive unit and integration tests -## ACME Versions +## Usage -There are two versions of the ACME protocol specification, ACME v1 and ACME v2. - -ACME v1 is currently in production. It is supported by _acme4j_ < 2.0, so **use _acme4j_ < 2.0 for production purposes!** - -_Let's Encrypt_ plans to launch an ACME v2 production server in the near future. A staging server is already available. _acme4j_ >= 2.0 supports the ACME v2 protocol. - -_Let's Encrypt_ has not announced a sunset date for ACME v1 yet, so there is plenty of time for migration. +* See the [online documentation](https://shredzone.org/maven/acme4j/) about how to use _acme4j_. +* For a quick start, have a look at [the source code of an example](https://github.com/shred/acme4j/blob/master/acme4j-example/src/main/java/org/shredzone/acme4j/ClientTest.java). +* The previous ACME v1 protocol is still supported by [_acme4j_ version 1](https://shredzone.org/maven/acme4j-acmev1/index.html). ## Known Issues * The _acme4j_ v2 API is still subject to change. * Integration tests do not fully cover all functions. The standard methods for creating an account, ordering, and downloading a certificate are tested. Other methods are not tested yet, and may not work as expected. -## Usage - -* See the [online documentation](https://shredzone.org/maven/acme4j/) about how to use _acme4j_. -* For a quick start, have a look at [the source code of an example](https://github.com/shred/acme4j/blob/master/acme4j-example/src/main/java/org/shredzone/acme4j/ClientTest.java). - ## Contribute * Fork the [Source code at GitHub](https://github.com/shred/acme4j). Feel free to send pull requests. From 14484b9fc937656a29229a0252dd218e0dc70da0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Richard=20K=C3=B6rber?= Date: Tue, 13 Mar 2018 23:40:54 +0100 Subject: [PATCH 5/6] [maven-release-plugin] prepare release v2.0 --- acme4j-client/pom.xml | 2 +- acme4j-example/pom.xml | 2 +- acme4j-it/pom.xml | 4 ++-- acme4j-utils/pom.xml | 2 +- pom.xml | 4 ++-- 5 files changed, 7 insertions(+), 7 deletions(-) diff --git a/acme4j-client/pom.xml b/acme4j-client/pom.xml index a6daac23..9f469509 100644 --- a/acme4j-client/pom.xml +++ b/acme4j-client/pom.xml @@ -20,7 +20,7 @@ org.shredzone.acme4j acme4j - 2.0-SNAPSHOT + 2.0 acme4j-client diff --git a/acme4j-example/pom.xml b/acme4j-example/pom.xml index 0991a0d3..2e39bf6a 100644 --- a/acme4j-example/pom.xml +++ b/acme4j-example/pom.xml @@ -20,7 +20,7 @@ org.shredzone.acme4j acme4j - 2.0-SNAPSHOT + 2.0 acme4j-example diff --git a/acme4j-it/pom.xml b/acme4j-it/pom.xml index 8ee35a81..2851e9a0 100644 --- a/acme4j-it/pom.xml +++ b/acme4j-it/pom.xml @@ -20,7 +20,7 @@ org.shredzone.acme4j acme4j - 2.0-SNAPSHOT + 2.0 acme4j-it @@ -131,7 +131,7 @@ - echo "nameserver $(grep 'bammbamm' /etc/hosts|cut -f1)">/etc/resolv.conf; \ + echo "nameserver $(grep 'bammbamm' /etc/hosts|cut -f1)">/etc/resolv.conf; \ pebble -config /etc/pebble/pebble-config.json diff --git a/acme4j-utils/pom.xml b/acme4j-utils/pom.xml index 492096c1..e57da4d3 100644 --- a/acme4j-utils/pom.xml +++ b/acme4j-utils/pom.xml @@ -20,7 +20,7 @@ org.shredzone.acme4j acme4j - 2.0-SNAPSHOT + 2.0 acme4j-utils diff --git a/pom.xml b/pom.xml index 5c0caa45..be914061 100644 --- a/pom.xml +++ b/pom.xml @@ -19,7 +19,7 @@ org.shredzone.acme4j acme4j - 2.0-SNAPSHOT + 2.0 pom acme4j @@ -37,7 +37,7 @@ https://github.com/shred/acme4j/ scm:git:git@github.com:shred/acme4j.git scm:git:git@github.com:shred/acme4j.git - HEAD + v2.0 GitHub From a8d3f86f85980c769f4fc1b10edea781b52a1cb9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Richard=20K=C3=B6rber?= Date: Tue, 13 Mar 2018 23:40:55 +0100 Subject: [PATCH 6/6] [maven-release-plugin] prepare for next development iteration --- acme4j-client/pom.xml | 2 +- acme4j-example/pom.xml | 2 +- acme4j-it/pom.xml | 2 +- acme4j-utils/pom.xml | 2 +- pom.xml | 4 ++-- 5 files changed, 6 insertions(+), 6 deletions(-) diff --git a/acme4j-client/pom.xml b/acme4j-client/pom.xml index 9f469509..44325361 100644 --- a/acme4j-client/pom.xml +++ b/acme4j-client/pom.xml @@ -20,7 +20,7 @@ org.shredzone.acme4j acme4j - 2.0 + 2.1-SNAPSHOT acme4j-client diff --git a/acme4j-example/pom.xml b/acme4j-example/pom.xml index 2e39bf6a..a713ccb7 100644 --- a/acme4j-example/pom.xml +++ b/acme4j-example/pom.xml @@ -20,7 +20,7 @@ org.shredzone.acme4j acme4j - 2.0 + 2.1-SNAPSHOT acme4j-example diff --git a/acme4j-it/pom.xml b/acme4j-it/pom.xml index 2851e9a0..d71fd04f 100644 --- a/acme4j-it/pom.xml +++ b/acme4j-it/pom.xml @@ -20,7 +20,7 @@ org.shredzone.acme4j acme4j - 2.0 + 2.1-SNAPSHOT acme4j-it diff --git a/acme4j-utils/pom.xml b/acme4j-utils/pom.xml index e57da4d3..778d9892 100644 --- a/acme4j-utils/pom.xml +++ b/acme4j-utils/pom.xml @@ -20,7 +20,7 @@ org.shredzone.acme4j acme4j - 2.0 + 2.1-SNAPSHOT acme4j-utils diff --git a/pom.xml b/pom.xml index be914061..256aca08 100644 --- a/pom.xml +++ b/pom.xml @@ -19,7 +19,7 @@ org.shredzone.acme4j acme4j - 2.0 + 2.1-SNAPSHOT pom acme4j @@ -37,7 +37,7 @@ https://github.com/shred/acme4j/ scm:git:git@github.com:shred/acme4j.git scm:git:git@github.com:shred/acme4j.git - v2.0 + HEAD GitHub