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 adec80ee..c02f0d9e 100644 --- a/acme4j-client/src/main/java/org/shredzone/acme4j/Account.java +++ b/acme4j-client/src/main/java/org/shredzone/acme4j/Account.java @@ -22,6 +22,7 @@ import java.security.KeyPair; import java.time.Instant; import java.util.ArrayList; import java.util.Arrays; +import java.util.Collection; import java.util.Collections; import java.util.Iterator; import java.util.List; @@ -141,21 +142,36 @@ public class Account extends AcmeResource { /** * Orders a certificate. The certificate will be associated with this account. * - * @param csr - * CSR containing the parameters for the certificate being requested + * @param domains + * Domains of the certificate to be ordered. May contain wildcard domains. + * IDN names are accepted and will be ACE encoded automatically. * @param notBefore * Requested "notBefore" date in the certificate, or {@code null} * @param notAfter * Requested "notAfter" date in the certificate, or {@code null} * @return {@link Order} object for this domain */ - public Order orderCertificate(byte[] csr, Instant notBefore, Instant notAfter) throws AcmeException { - Objects.requireNonNull(csr, "csr"); + public Order orderCertificate(Collection domains, Instant notBefore, Instant notAfter) + throws AcmeException { + Objects.requireNonNull(domains, "domains"); + if (domains.isEmpty()) { + throw new IllegalArgumentException("Cannot order an empty collection of domains"); + } + + Object[] identifiers = new Object[domains.size()]; + Iterator di = domains.iterator(); + for (int ix = 0; ix < identifiers.length; ix++) { + identifiers[ix] = new JSONBuilder() + .put("type", "dns") + .put("value", toAce(di.next())) + .toMap(); + } LOG.debug("orderCertificate"); try (Connection conn = getSession().provider().connect()) { JSONBuilder claims = new JSONBuilder(); - claims.putBase64("csr", csr); + claims.array("identifiers", identifiers); + if (notBefore != null) { claims.put("notBefore", notBefore); } 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 ac757ead..4170ab4a 100644 --- a/acme4j-client/src/main/java/org/shredzone/acme4j/Order.java +++ b/acme4j-client/src/main/java/org/shredzone/acme4j/Order.java @@ -24,6 +24,7 @@ import org.shredzone.acme4j.connector.Connection; import org.shredzone.acme4j.exception.AcmeException; import org.shredzone.acme4j.exception.AcmeLazyLoadingException; import org.shredzone.acme4j.toolbox.JSON; +import org.shredzone.acme4j.toolbox.JSONBuilder; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -36,11 +37,12 @@ public class Order extends AcmeResource { private Status status; private Instant expires; - private byte[] csr; + private List identifiers; private Instant notBefore; private Instant notAfter; private Problem error; private List authorizations; + private URL finalizeUrl; private Certificate certificate; private boolean loaded = false; @@ -87,11 +89,10 @@ public class Order extends AcmeResource { } /** - * Gets the CSR that was used for the order. + * Gets the list of domain names to be ordered. */ - public byte[] getCsr() { - load(); - return csr; + public List getDomains() { + return identifiers; } /** @@ -121,6 +122,42 @@ public class Order extends AcmeResource { .collect(toList()); } + /** + * Gets the location {@link URL} of where to send the finalization call to. + *

+ * For internal purposes. Use {@link #execute(byte[])} to finalize an order. + */ + public URL getFinalizeLocation() { + load(); + return finalizeUrl; + } + + /** + * Finalizes the order, by providing a CSR. + *

+ * After a successful finalization, the certificate is available at + * {@link #getCertificate()}. + *

+ * Even though the ACME protocol uses the term "finalize an order", this method is + * called {@link #execute(byte[])} to avoid confusion with the general + * {@link Object#finalize()} method. + * + * @param csr + * CSR containing the parameters for the certificate being requested, in + * DER format + */ + public void execute(byte[] csr) throws AcmeException { + LOG.debug("finalize"); + try (Connection conn = getSession().provider().connect()) { + JSONBuilder claims = new JSONBuilder(); + claims.putBase64("csr", csr); + + conn.sendSignedRequest(getFinalizeLocation(), claims, getSession()); + conn.accept(HttpURLConnection.HTTP_OK); + } + loaded = false; // invalidate this object + } + /** * Gets the {@link Certificate} if it is available. {@code null} otherwise. */ @@ -165,15 +202,20 @@ public class Order extends AcmeResource { public void unmarshal(JSON json) { this.status = json.get("status").asStatusOrElse(Status.UNKNOWN); this.expires = json.get("expires").asInstant(); - this.csr = json.get("csr").asBinary(); this.notBefore = json.get("notBefore").asInstant(); this.notAfter = json.get("notAfter").asInstant(); + this.finalizeUrl = json.get("finalizeURL").asURL(); URL certUrl = json.get("certificate").asURL(); certificate = certUrl != null ? Certificate.bind(getSession(), certUrl) : null; this.error = json.get("error").asProblem(getLocation()); + this.identifiers = json.get("identifiers").asArray().stream() + .map(JSON.Value::asObject) + .map(it -> it.get("value").asString()) + .collect(toList()); + this.authorizations = json.get("authorizations").asArray().stream() .map(JSON.Value::asURL) .collect(toList()); diff --git a/acme4j-client/src/test/java/org/shredzone/acme4j/AccountTest.java b/acme4j-client/src/test/java/org/shredzone/acme4j/AccountTest.java index c4c05da6..8898558e 100644 --- a/acme4j-client/src/test/java/org/shredzone/acme4j/AccountTest.java +++ b/acme4j-client/src/test/java/org/shredzone/acme4j/AccountTest.java @@ -190,7 +190,6 @@ public class AccountTest { */ @Test public void testOrderCertificate() throws Exception { - byte[] csr = TestUtils.getResourceAsByteArray("/csr.der"); Instant notBefore = parseTimestamp("2016-01-01T00:00:00Z"); Instant notAfter = parseTimestamp("2016-01-08T00:00:00Z"); @@ -224,9 +223,11 @@ public class AccountTest { provider.putTestResource(Resource.NEW_ORDER, resourceUrl); Account account = new Account(session, locationUrl); - Order order = account.orderCertificate(csr, notBefore, notAfter); + Order order = account.orderCertificate( + Arrays.asList("example.com", "www.example.com"), + notBefore, notAfter); - assertThat(order.getCsr(), is(csr)); + assertThat(order.getDomains(), containsInAnyOrder("example.com", "www.example.com")); assertThat(order.getNotBefore(), is(parseTimestamp("2016-01-01T00:10:00Z"))); assertThat(order.getNotAfter(), is(parseTimestamp("2016-01-08T00:10:00Z"))); assertThat(order.getExpires(), is(parseTimestamp("2016-01-10T00:00:00Z"))); 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 90bedf0d..e3364f86 100644 --- a/acme4j-client/src/test/java/org/shredzone/acme4j/OrderTest.java +++ b/acme4j-client/src/test/java/org/shredzone/acme4j/OrderTest.java @@ -17,6 +17,7 @@ import static org.hamcrest.Matchers.*; import static org.junit.Assert.assertThat; import static org.shredzone.acme4j.toolbox.AcmeUtils.parseTimestamp; import static org.shredzone.acme4j.toolbox.TestUtils.*; +import static uk.co.datumedge.hamcrest.json.SameJSONAs.sameJSONAs; import java.net.HttpURLConnection; import java.net.URI; @@ -28,6 +29,7 @@ import org.junit.Test; import org.shredzone.acme4j.exception.AcmeException; import org.shredzone.acme4j.provider.TestableConnectionProvider; import org.shredzone.acme4j.toolbox.JSON; +import org.shredzone.acme4j.toolbox.JSONBuilder; import org.shredzone.acme4j.toolbox.TestUtils; /** @@ -36,14 +38,13 @@ import org.shredzone.acme4j.toolbox.TestUtils; public class OrderTest { private URL locationUrl = url("http://example.com/acme/order/1234"); + private URL finalizeUrl = url("https://example.com/acme/acct/1/order/1/finalize"); /** * Test that order is properly updated. */ @Test public void testUpdate() throws Exception { - byte[] csr = TestUtils.getResourceAsByteArray("/csr.der"); - TestableConnectionProvider provider = new TestableConnectionProvider() { @Override public void sendRequest(URL url, Session session) { @@ -71,10 +72,11 @@ public class OrderTest { assertThat(order.getExpires(), is(parseTimestamp("2015-03-01T14:09:00Z"))); assertThat(order.getLocation(), is(locationUrl)); + assertThat(order.getDomains(), containsInAnyOrder("example.com", "www.example.com")); 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.getCsr(), is(csr)); + assertThat(order.getFinalizeLocation(), is(finalizeUrl)); assertThat(order.getError(), is(notNullValue())); assertThat(order.getError().getType(), is(URI.create("urn:ietf:params:acme:error:connection"))); @@ -135,4 +137,64 @@ public class OrderTest { provider.close(); } + /** + * Test that order is properly finalized. + */ + @Test + public void testFinalize() throws Exception { + byte[] csr = TestUtils.getResourceAsByteArray("/csr.der"); + + TestableConnectionProvider provider = new TestableConnectionProvider() { + private boolean isFinalized = false; + + @Override + public void sendRequest(URL url, Session session) { + assertThat(url, is(locationUrl)); + } + + @Override + public void sendSignedRequest(URL url, JSONBuilder claims, Session session) { + assertThat(url, is(finalizeUrl)); + assertThat(claims.toString(), sameJSONAs(getJSON("finalizeRequest").toString())); + assertThat(session, is(notNullValue())); + isFinalized = true; + } + + @Override + public int accept(int... httpStatus) throws AcmeException { + assertThat(httpStatus, isIntArrayContainingInAnyOrder(HttpURLConnection.HTTP_OK)); + return HttpURLConnection.HTTP_OK; + } + + @Override + public JSON readJsonResponse() { + return getJSON(isFinalized ? "finalizeResponse" : "updateOrderResponse"); + } + }; + + Session session = provider.createSession(); + + Order order = new Order(session, locationUrl); + order.execute(csr); + + assertThat(order.getStatus(), is(Status.VALID)); + assertThat(order.getExpires(), is(parseTimestamp("2015-03-01T14:09:00Z"))); + assertThat(order.getLocation(), is(locationUrl)); + + assertThat(order.getDomains(), containsInAnyOrder("example.com", "www.example.com")); + 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.getFinalizeLocation(), is(finalizeUrl)); + + List auths = order.getAuthorizations(); + assertThat(auths.size(), is(2)); + assertThat(auths.stream().map(Authorization::getLocation)::iterator, + containsInAnyOrder( + url("https://example.com/acme/authz/1234"), + url("https://example.com/acme/authz/2345"))); + + provider.close(); + } + } diff --git a/acme4j-client/src/test/resources/json/finalizeRequest.json b/acme4j-client/src/test/resources/json/finalizeRequest.json new file mode 100644 index 00000000..b3bfb13c --- /dev/null +++ b/acme4j-client/src/test/resources/json/finalizeRequest.json @@ -0,0 +1,3 @@ +{ + "csr": "MIIChDCCAWwCAQAwFjEUMBIGA1UEAwwLZXhhbXBsZS5jb20wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCPemmumcNGR0hsPo-2N6nkJ0FcEMdb0_MlucHR0dNeHEvn8vmcQHlYRjkDVX0aypnfKQI3tvhTBKLdlNvbVIW1TQ_Wbqh9TQlC8G3Hog8nRQ2vAzO4sH6nhvdrAFUmq6hkATpU3iQuDvtYu03ExaYHKsItLocl1OndaQizBn5udBv1baOW3Kd790k6lEWGrD-TXo6uwuMha2k_YBGNKd4S4UuPmbPV9SUVW8JSylBSgDhvY3BHv-dfdIMhVwRMZDFaa0mHDIYUiwcEaU5x4P6Q5bGP2wxcUPCLwFsbAK5K6B2T2P3A2fNjGBAlHwEkg6VMvi7jax8MD-oRnku2M2JLAgMBAAGgKTAnBgkqhkiG9w0BCQ4xGjAYMBYGA1UdEQQPMA2CC2V4YW1wbGUuY29tMA0GCSqGSIb3DQEBCwUAA4IBAQACnMZdjE1jVqnkHkEeGrMXujWuwuiKLZVa4YZ9fL0UIGOqqf4b9-3JmtEiLB9ycQO5N9rW4V-6_DBMeoeRBLu-wranHnxU4ds6GzNzBxKfI86_8t5pdQK4Cglv7yfseseZRdQtvcR2ejkW0F3SL1DF5Sk3T46aRYiUXxeoNC4Uh3zoIHOv8YGUa-DuZQ6OnHMhPrdsfU09L7KVAMTq1bodjGWmgoIJm4x5JSm19GbhYAm9Q9XWnN56YHqgS3FtS9n3wDxz7Dvo24whto1tUU5hnjrp31rTvyxG3kydoEZf2Ciq_82bQDb40kwnoO6RytPYJVMRIBsP2mCfaFtIt9Eb" +} \ No newline at end of file diff --git a/acme4j-client/src/test/resources/json/finalizeResponse.json b/acme4j-client/src/test/resources/json/finalizeResponse.json new file mode 100644 index 00000000..0f003ca4 --- /dev/null +++ b/acme4j-client/src/test/resources/json/finalizeResponse.json @@ -0,0 +1,22 @@ +{ + "status": "valid", + "expires": "2015-03-01T14:09:00Z", + "identifiers": [ + { + "type": "dns", + "value": "example.com" + }, + { + "type": "dns", + "value": "www.example.com" + } + ], + "notBefore": "2016-01-01T00:00:00Z", + "notAfter": "2016-01-08T00:00:00Z", + "authorizations": [ + "https://example.com/acme/authz/1234", + "https://example.com/acme/authz/2345" + ], + "finalizeURL": "https://example.com/acme/acct/1/order/1/finalize", + "certificate": "https://example.com/acme/cert/1234" +} diff --git a/acme4j-client/src/test/resources/json/requestOrderRequest.json b/acme4j-client/src/test/resources/json/requestOrderRequest.json index c6666c54..c5e4ef95 100644 --- a/acme4j-client/src/test/resources/json/requestOrderRequest.json +++ b/acme4j-client/src/test/resources/json/requestOrderRequest.json @@ -1,5 +1,14 @@ { - "csr": "MIIChDCCAWwCAQAwFjEUMBIGA1UEAwwLZXhhbXBsZS5jb20wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCPemmumcNGR0hsPo-2N6nkJ0FcEMdb0_MlucHR0dNeHEvn8vmcQHlYRjkDVX0aypnfKQI3tvhTBKLdlNvbVIW1TQ_Wbqh9TQlC8G3Hog8nRQ2vAzO4sH6nhvdrAFUmq6hkATpU3iQuDvtYu03ExaYHKsItLocl1OndaQizBn5udBv1baOW3Kd790k6lEWGrD-TXo6uwuMha2k_YBGNKd4S4UuPmbPV9SUVW8JSylBSgDhvY3BHv-dfdIMhVwRMZDFaa0mHDIYUiwcEaU5x4P6Q5bGP2wxcUPCLwFsbAK5K6B2T2P3A2fNjGBAlHwEkg6VMvi7jax8MD-oRnku2M2JLAgMBAAGgKTAnBgkqhkiG9w0BCQ4xGjAYMBYGA1UdEQQPMA2CC2V4YW1wbGUuY29tMA0GCSqGSIb3DQEBCwUAA4IBAQACnMZdjE1jVqnkHkEeGrMXujWuwuiKLZVa4YZ9fL0UIGOqqf4b9-3JmtEiLB9ycQO5N9rW4V-6_DBMeoeRBLu-wranHnxU4ds6GzNzBxKfI86_8t5pdQK4Cglv7yfseseZRdQtvcR2ejkW0F3SL1DF5Sk3T46aRYiUXxeoNC4Uh3zoIHOv8YGUa-DuZQ6OnHMhPrdsfU09L7KVAMTq1bodjGWmgoIJm4x5JSm19GbhYAm9Q9XWnN56YHqgS3FtS9n3wDxz7Dvo24whto1tUU5hnjrp31rTvyxG3kydoEZf2Ciq_82bQDb40kwnoO6RytPYJVMRIBsP2mCfaFtIt9Eb", + "identifiers": [ + { + "type": "dns", + "value": "example.com" + }, + { + "type": "dns", + "value": "www.example.com" + } + ], "notBefore": "2016-01-01T00:00:00Z", "notAfter": "2016-01-08T00:00:00Z" } diff --git a/acme4j-client/src/test/resources/json/requestOrderResponse.json b/acme4j-client/src/test/resources/json/requestOrderResponse.json index c4dbd03a..4f9b4f3f 100644 --- a/acme4j-client/src/test/resources/json/requestOrderResponse.json +++ b/acme4j-client/src/test/resources/json/requestOrderResponse.json @@ -1,11 +1,21 @@ { "status": "pending", "expires": "2016-01-10T00:00:00Z", - "csr": "MIIChDCCAWwCAQAwFjEUMBIGA1UEAwwLZXhhbXBsZS5jb20wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCPemmumcNGR0hsPo-2N6nkJ0FcEMdb0_MlucHR0dNeHEvn8vmcQHlYRjkDVX0aypnfKQI3tvhTBKLdlNvbVIW1TQ_Wbqh9TQlC8G3Hog8nRQ2vAzO4sH6nhvdrAFUmq6hkATpU3iQuDvtYu03ExaYHKsItLocl1OndaQizBn5udBv1baOW3Kd790k6lEWGrD-TXo6uwuMha2k_YBGNKd4S4UuPmbPV9SUVW8JSylBSgDhvY3BHv-dfdIMhVwRMZDFaa0mHDIYUiwcEaU5x4P6Q5bGP2wxcUPCLwFsbAK5K6B2T2P3A2fNjGBAlHwEkg6VMvi7jax8MD-oRnku2M2JLAgMBAAGgKTAnBgkqhkiG9w0BCQ4xGjAYMBYGA1UdEQQPMA2CC2V4YW1wbGUuY29tMA0GCSqGSIb3DQEBCwUAA4IBAQACnMZdjE1jVqnkHkEeGrMXujWuwuiKLZVa4YZ9fL0UIGOqqf4b9-3JmtEiLB9ycQO5N9rW4V-6_DBMeoeRBLu-wranHnxU4ds6GzNzBxKfI86_8t5pdQK4Cglv7yfseseZRdQtvcR2ejkW0F3SL1DF5Sk3T46aRYiUXxeoNC4Uh3zoIHOv8YGUa-DuZQ6OnHMhPrdsfU09L7KVAMTq1bodjGWmgoIJm4x5JSm19GbhYAm9Q9XWnN56YHqgS3FtS9n3wDxz7Dvo24whto1tUU5hnjrp31rTvyxG3kydoEZf2Ciq_82bQDb40kwnoO6RytPYJVMRIBsP2mCfaFtIt9Eb", + "identifiers": [ + { + "type": "dns", + "value": "example.com" + }, + { + "type": "dns", + "value": "www.example.com" + } + ], "notBefore": "2016-01-01T00:10:00Z", "notAfter": "2016-01-08T00:10:00Z", "authorizations": [ "https://example.com/acme/authz/1234", "https://example.com/acme/authz/2345" - ] + ], + "finalizeURL": "https://example.com/acme/acct/1/order/1/finalize" } diff --git a/acme4j-client/src/test/resources/json/updateOrderResponse.json b/acme4j-client/src/test/resources/json/updateOrderResponse.json index de6a8dca..d3eec3cd 100644 --- a/acme4j-client/src/test/resources/json/updateOrderResponse.json +++ b/acme4j-client/src/test/resources/json/updateOrderResponse.json @@ -1,13 +1,23 @@ { "status": "pending", "expires": "2015-03-01T14:09:00Z", - "csr": "MIIChDCCAWwCAQAwFjEUMBIGA1UEAwwLZXhhbXBsZS5jb20wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCPemmumcNGR0hsPo-2N6nkJ0FcEMdb0_MlucHR0dNeHEvn8vmcQHlYRjkDVX0aypnfKQI3tvhTBKLdlNvbVIW1TQ_Wbqh9TQlC8G3Hog8nRQ2vAzO4sH6nhvdrAFUmq6hkATpU3iQuDvtYu03ExaYHKsItLocl1OndaQizBn5udBv1baOW3Kd790k6lEWGrD-TXo6uwuMha2k_YBGNKd4S4UuPmbPV9SUVW8JSylBSgDhvY3BHv-dfdIMhVwRMZDFaa0mHDIYUiwcEaU5x4P6Q5bGP2wxcUPCLwFsbAK5K6B2T2P3A2fNjGBAlHwEkg6VMvi7jax8MD-oRnku2M2JLAgMBAAGgKTAnBgkqhkiG9w0BCQ4xGjAYMBYGA1UdEQQPMA2CC2V4YW1wbGUuY29tMA0GCSqGSIb3DQEBCwUAA4IBAQACnMZdjE1jVqnkHkEeGrMXujWuwuiKLZVa4YZ9fL0UIGOqqf4b9-3JmtEiLB9ycQO5N9rW4V-6_DBMeoeRBLu-wranHnxU4ds6GzNzBxKfI86_8t5pdQK4Cglv7yfseseZRdQtvcR2ejkW0F3SL1DF5Sk3T46aRYiUXxeoNC4Uh3zoIHOv8YGUa-DuZQ6OnHMhPrdsfU09L7KVAMTq1bodjGWmgoIJm4x5JSm19GbhYAm9Q9XWnN56YHqgS3FtS9n3wDxz7Dvo24whto1tUU5hnjrp31rTvyxG3kydoEZf2Ciq_82bQDb40kwnoO6RytPYJVMRIBsP2mCfaFtIt9Eb", + "identifiers": [ + { + "type": "dns", + "value": "example.com" + }, + { + "type": "dns", + "value": "www.example.com" + } + ], "notBefore": "2016-01-01T00:00:00Z", "notAfter": "2016-01-08T00:00:00Z", "authorizations": [ "https://example.com/acme/authz/1234", "https://example.com/acme/authz/2345" ], + "finalizeURL": "https://example.com/acme/acct/1/order/1/finalize", "certificate": "https://example.com/acme/cert/1234", "error": { "type": "urn:ietf:params:acme:error:connection", 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 91995572..5e5b2f11 100644 --- a/acme4j-example/src/main/java/org/shredzone/acme4j/ClientTest.java +++ b/acme4j-example/src/main/java/org/shredzone/acme4j/ClientTest.java @@ -90,6 +90,14 @@ public class ClientTest { // Load or create a key pair for the domains. This should not be the userKeyPair! KeyPair domainKeyPair = loadOrCreateDomainKeyPair(); + // Order the certificate + Order order = acct.orderCertificate(domains, null, null); + + // Perform all required authorizations + for (Authorization auth : order.getAuthorizations()) { + authorize(auth); + } + // Generate a CSR for all of the domains, and sign it with the domain key pair. CSRBuilder csrb = new CSRBuilder(); csrb.addDomains(domains); @@ -100,16 +108,8 @@ public class ClientTest { csrb.write(out); } - // Order the certificate - Order order = acct.orderCertificate(csrb.getEncoded(), null, null); - - // Perform all required authorizations - for (Authorization auth : order.getAuthorizations()) { - authorize(auth); - } - // Get the certificate - order.update(); + order.execute(csrb.getEncoded()); Certificate certificate = order.getCertificate(); LOG.info("Success! The certificate for domains " + domains + " has been generated!"); diff --git a/acme4j-it/src/test/java/org/shredzone/acme4j/it/OrderIT.java b/acme4j-it/src/test/java/org/shredzone/acme4j/it/OrderIT.java index c1ffffb2..75acda72 100644 --- a/acme4j-it/src/test/java/org/shredzone/acme4j/it/OrderIT.java +++ b/acme4j-it/src/test/java/org/shredzone/acme4j/it/OrderIT.java @@ -23,6 +23,7 @@ import java.security.cert.X509Certificate; import java.time.Duration; import java.time.Instant; import java.time.temporal.ChronoUnit; +import java.util.Arrays; import org.junit.Test; import org.shredzone.acme4j.Account; @@ -134,16 +135,10 @@ public class OrderIT extends PebbleITBase { KeyPair domainKeyPair = createKeyPair(); - CSRBuilder csr = new CSRBuilder(); - csr.addDomain(domain); - csr.sign(domainKeyPair); - byte[] encodedCsr = csr.getEncoded(); - Instant notBefore = Instant.now().truncatedTo(ChronoUnit.MILLIS); Instant notAfter = notBefore.plus(Duration.ofDays(20L)); - Order order = account.orderCertificate(encodedCsr, notBefore, notAfter); - assertThat(order.getCsr(), is(encodedCsr)); + Order order = account.orderCertificate(Arrays.asList(domain), notBefore, notAfter); assertThat(order.getNotBefore(), is(notBefore)); assertThat(order.getNotAfter(), is(notAfter)); assertThat(order.getStatus(), is(Status.PENDING)); @@ -166,7 +161,12 @@ public class OrderIT extends PebbleITBase { } } - order.update(); + CSRBuilder csr = new CSRBuilder(); + csr.addDomain(domain); + csr.sign(domainKeyPair); + byte[] encodedCsr = csr.getEncoded(); + + order.execute(encodedCsr); Certificate certificate = order.getCertificate(); X509Certificate cert = certificate.getCertificate(); diff --git a/src/site/markdown/usage/order.md b/src/site/markdown/usage/order.md index ce9b9028..1bec7e35 100644 --- a/src/site/markdown/usage/order.md +++ b/src/site/markdown/usage/order.md @@ -2,45 +2,14 @@ Once you have your account set up, you are ready to order certificates. -## Create a Certificate Signing Request (CSR) - -To do so, prepare a PKCS#10 CSR file. A single domain may be set as _Common Name_. Multiple domains must be provided as _Subject Alternative Name_. Other properties (_Organization_, _Organization Unit_ etc.) depend on the CA. Some may require these properties to be set, while others may ignore them when generating the certificate. - -CSR files can be generated with command line tools like `openssl`. Unfortunately the standard Java does not offer classes for that, so you'd have to resort to [Bouncy Castle](http://www.bouncycastle.org/java.html) if you want to create a CSR programmatically. In the `acme4j-utils` module, there is a [`CSRBuilder`](../apidocs/org/shredzone/acme4j/util/CSRBuilder.html) for your convenience. You can also use [`KeyPairUtils`](../apidocs/org/shredzone/acme4j/util/KeyPairUtils.html) for generating a new key pair for your domain. - -> __Important:__ Do not just use your account key pair as domain key pair, but always generate separate key pairs! - -```java -KeyPair domainKeyPair = ... // KeyPair to be used for HTTPS encryption - -CSRBuilder csrb = new CSRBuilder(); -csrb.addDomain("example.org"); -csrb.addDomain("www.example.org"); -csrb.addDomain("m.example.org"); -csrb.setOrganization("The Example Organization") -csrb.sign(domainKeyPair); -byte[] csr = csrb.getEncoded(); -``` - -It is a good idea to store the generated CSR somewhere, as you will need it again for renewal: - -```java -csrb.write(new FileWriter("example.csr")); -``` - -The generated certificate will be valid for all of the domains stored in the CSR. - -> __Note:__ The number of domains per certificate may be limited. See your CA's documentation for the limits. - -## Order a Certificate - -The next step is to use your `Account` object to order the certificate, by using the `orderCertificate()` method. You can optionally give your desired `notBefore` and `notAfter` dates for the generated certificate, but it is at the discretion of the CA to use (or ignore) these values. +Use your `Account` object to order the certificate, by using the `orderCertificate()` method. It requires a collection of domain names to be ordered. You can optionally give your desired `notBefore` and `notAfter` dates for the generated certificate, but it is at the discretion of the CA to use (or ignore) these values. ```java Account account = ... // your Account object -byte[] csr = ... // your CSR (see above) -Order order = account.orderCertificate(csr, null, null); +Order order = account.orderCertificate( + Arrays.of("example.org", "www.example.org", "m.example.org"), + null, null); ``` The `Order` resource contains a collection of `Authorization`s that can be read from the `getAuthorizations()` method. You must process _all of them_ in order to get the certificate. @@ -84,6 +53,40 @@ The CA server may start the validation immediately after `trigger()` is invoked, When the challenge status is `VALID`, you have successfully authorized your domain. +## Finalize the Order + +After successfully completing all authorizations, the order needs to be finalized by a PKCS#10 CSR file. A single domain may be set as _Common Name_. Multiple domains must be provided as _Subject Alternative Name_. You must provide exactly the domains that you had passed to the `order()` method above, otherwise the finalization will fail. It depends on the CA if other CSR properties (_Organization_, _Organization Unit_ etc.) are accepted. Some may require these properties to be set, while others may ignore them when generating the certificate. + +CSR files can be generated with command line tools like `openssl`. Unfortunately the standard Java does not offer classes for that, so you'd have to resort to [Bouncy Castle](http://www.bouncycastle.org/java.html) if you want to create a CSR programmatically. In the `acme4j-utils` module, there is a [`CSRBuilder`](../apidocs/org/shredzone/acme4j/util/CSRBuilder.html) for your convenience. You can also use [`KeyPairUtils`](../apidocs/org/shredzone/acme4j/util/KeyPairUtils.html) for generating a new key pair for your domain. + +> __Important:__ Do not just use your account key pair as domain key pair, but always generate separate key pairs! + +```java +KeyPair domainKeyPair = ... // KeyPair to be used for HTTPS encryption + +CSRBuilder csrb = new CSRBuilder(); +csrb.addDomain("example.org"); +csrb.addDomain("www.example.org"); +csrb.addDomain("m.example.org"); +csrb.setOrganization("The Example Organization") +csrb.sign(domainKeyPair); +byte[] csr = csrb.getEncoded(); +``` + +It is a good idea to store the generated CSR somewhere, as you will need it again for renewal: + +```java +csrb.write(new FileWriter("example.csr")); +``` + +After that, finalize the order: + +```java +order.execute(csr); +``` + +> __Note:__ The number of domains per certificate may be limited. See your CA's documentation for the limits. + ## Wildcard Certificates You can also generate a wildcard certificate that is valid for all subdomains of a domain, by prefixing the domain name with `*.` (e.g. `*.example.org`). The domain itself is not covered by the wildcard certificate, and also needs to be added to the CSR if necessary. @@ -104,7 +107,7 @@ byte[] csr = csrb.getEncoded(); In the subsequent authorization process, you would have to prove ownership of the `example.org` domain. -> __Note:__ Some CAs may reject wildcard certificate orders, or may involve `Challenge`s that are not documented here. Refer to your CA's documentation to find out about the wildcard certificate policy. +> __Note:__ Some CAs may reject wildcard certificate orders, may only offer a limited set of `Challenge`s, or may involve `Challenge`s that are not documented here. Refer to your CA's documentation to find out about the wildcard certificate policy. > __Note:__ _acme4j_ accepts all kind of wildcard notations (e.g. `www.*.example.org`, `*.*.example.org`.). However, those notations are not specified and may be rejected by your CA.