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 10595fd6..73916c73 100644 --- a/acme4j-client/src/main/java/org/shredzone/acme4j/Order.java +++ b/acme4j-client/src/main/java/org/shredzone/acme4j/Order.java @@ -15,18 +15,23 @@ package org.shredzone.acme4j; import static java.util.stream.Collectors.toUnmodifiableList; +import java.io.IOException; import java.net.URL; +import java.security.KeyPair; import java.time.Duration; import java.time.Instant; import java.util.List; import java.util.Optional; +import java.util.function.Consumer; import edu.umd.cs.findbugs.annotations.Nullable; +import org.bouncycastle.pkcs.PKCS10CertificationRequest; import org.shredzone.acme4j.exception.AcmeException; import org.shredzone.acme4j.exception.AcmeNotSupportedException; import org.shredzone.acme4j.toolbox.JSON; import org.shredzone.acme4j.toolbox.JSON.Value; import org.shredzone.acme4j.toolbox.JSONBuilder; +import org.shredzone.acme4j.util.CSRBuilder; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -161,18 +166,91 @@ public class Order extends AcmeJsonResource { } /** - * Finalizes the order, by providing a CSR. + * Finalizes the order. *

- * After a successful finalization, the certificate is available at + * If the finalization was successful, the certificate is provided via * {@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 problematic + * called {@link #execute(KeyPair)} to avoid confusion with the problematic * {@link Object#finalize()} method. * + * @param domainKeyPair + * The {@link KeyPair} that is going to be certified. This is not + * your account's keypair! + * @see #execute(KeyPair, Consumer) + * @see #execute(PKCS10CertificationRequest) + * @see #execute(byte[]) + * @since 3.0.0 + */ + public void execute(KeyPair domainKeyPair) throws AcmeException { + execute(domainKeyPair, csrBuilder -> {}); + } + + /** + * Finalizes the order (see {@link #execute(KeyPair)}). + *

+ * This method also accepts a builderConsumer that can be used to add further details + * to the CSR (e.g. your organization). The identifiers (IPs, domain names, etc.) are + * automatically added to the CSR. + * + * @param domainKeyPair + * The {@link KeyPair} that is going to be used together with the certificate. + * This is not your account's keypair! + * @param builderConsumer + * {@link Consumer} that adds further details to the provided + * {@link CSRBuilder}. + * @see #execute(KeyPair) + * @see #execute(PKCS10CertificationRequest) + * @see #execute(byte[]) + * @since 3.0.0 + */ + public void execute(KeyPair domainKeyPair, Consumer builderConsumer) throws AcmeException { + try { + var csrBuilder = new CSRBuilder(); + csrBuilder.addIdentifiers(getIdentifiers()); + builderConsumer.accept(csrBuilder); + csrBuilder.sign(domainKeyPair); + execute(csrBuilder.getCSR()); + } catch (IOException ex) { + throw new AcmeException("Failed to create CSR", ex); + } + } + + /** + * Finalizes the order (see {@link #execute(KeyPair)}). + *

+ * This method receives a {@link PKCS10CertificationRequest} instance of a CSR that + * was generated externally. Use this method to gain full control over the content of + * the CSR. The CSR is not checked by acme4j, but just transported to the CA. It is + * your responsibility that it matches to the order. + * * @param csr - * CSR containing the parameters for the certificate being requested, in DER - * format + * {@link PKCS10CertificationRequest} to be used for this order. + * @see #execute(KeyPair) + * @see #execute(KeyPair, Consumer) + * @see #execute(byte[]) + * @since 3.0.0 + */ + public void execute(PKCS10CertificationRequest csr) throws AcmeException { + try { + execute(csr.getEncoded()); + } catch (IOException ex) { + throw new AcmeException("Invalid CSR", ex); + } + } + + /** + * Finalizes the order (see {@link #execute(KeyPair)}). + *

+ * This method receives a byte array containing an encoded CSR that was generated + * externally. Use this method to gain full control over the content of the CSR. The + * CSR is not checked by acme4j, but just transported to the CA. It is your + * responsibility that it matches to the order. + * + * @param csr + * Binary representation of a CSR containing the parameters for the + * certificate being requested, in DER format */ public void execute(byte[] csr) throws AcmeException { LOG.debug("finalize"); diff --git a/acme4j-example/src/main/java/org/shredzone/acme4j/example/ClientTest.java b/acme4j-example/src/main/java/org/shredzone/acme4j/example/ClientTest.java index 5574c691..d16564b7 100644 --- a/acme4j-example/src/main/java/org/shredzone/acme4j/example/ClientTest.java +++ b/acme4j-example/src/main/java/org/shredzone/acme4j/example/ClientTest.java @@ -17,7 +17,6 @@ import java.io.File; import java.io.FileReader; import java.io.FileWriter; import java.io.IOException; -import java.io.Writer; import java.net.URI; import java.net.URL; import java.security.KeyPair; @@ -41,7 +40,6 @@ import org.shredzone.acme4j.challenge.Challenge; import org.shredzone.acme4j.challenge.Dns01Challenge; import org.shredzone.acme4j.challenge.Http01Challenge; import org.shredzone.acme4j.exception.AcmeException; -import org.shredzone.acme4j.util.CSRBuilder; import org.shredzone.acme4j.util.KeyPairUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -104,18 +102,8 @@ public class ClientTest { 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); - csrb.sign(domainKeyPair); - - // Write the CSR to a file, for later use. - try (Writer out = new FileWriter(DOMAIN_CSR_FILE)) { - csrb.write(out); - } - // Order the certificate - order.execute(csrb.getEncoded()); + order.execute(domainKeyPair); // Wait for the order to complete try { diff --git a/acme4j-it/src/test/java/org/shredzone/acme4j/it/boulder/OrderHttpIT.java b/acme4j-it/src/test/java/org/shredzone/acme4j/it/boulder/OrderHttpIT.java index 8fa97d24..25cca03b 100644 --- a/acme4j-it/src/test/java/org/shredzone/acme4j/it/boulder/OrderHttpIT.java +++ b/acme4j-it/src/test/java/org/shredzone/acme4j/it/boulder/OrderHttpIT.java @@ -30,7 +30,6 @@ import org.shredzone.acme4j.challenge.Http01Challenge; import org.shredzone.acme4j.exception.AcmeException; import org.shredzone.acme4j.exception.AcmeLazyLoadingException; import org.shredzone.acme4j.it.BammBammClient; -import org.shredzone.acme4j.util.CSRBuilder; import org.shredzone.acme4j.util.KeyPairUtils; /** @@ -79,12 +78,7 @@ public class OrderHttpIT { client.httpRemoveToken(challenge.getToken()); } - var csr = new CSRBuilder(); - csr.addDomain(TEST_DOMAIN); - csr.sign(domainKeyPair); - var encodedCsr = csr.getEncoded(); - - order.execute(encodedCsr); + order.execute(domainKeyPair); await() .pollInterval(1, SECONDS) diff --git a/acme4j-it/src/test/java/org/shredzone/acme4j/it/pebble/OrderIT.java b/acme4j-it/src/test/java/org/shredzone/acme4j/it/pebble/OrderIT.java index f9b44a01..d7f16b50 100644 --- a/acme4j-it/src/test/java/org/shredzone/acme4j/it/pebble/OrderIT.java +++ b/acme4j-it/src/test/java/org/shredzone/acme4j/it/pebble/OrderIT.java @@ -39,7 +39,6 @@ import org.shredzone.acme4j.challenge.Http01Challenge; import org.shredzone.acme4j.challenge.TlsAlpn01Challenge; import org.shredzone.acme4j.exception.AcmeException; import org.shredzone.acme4j.exception.AcmeServerException; -import org.shredzone.acme4j.util.CSRBuilder; /** * Tests a complete certificate order with different challenges. @@ -179,12 +178,7 @@ public class OrderIT extends PebbleITBase { assertThat(auth.getStatus()).isEqualTo(Status.VALID); } - var csr = new CSRBuilder(); - csr.addDomain(domain); - csr.sign(domainKeyPair); - var encodedCsr = csr.getEncoded(); - - order.execute(encodedCsr); + order.execute(domainKeyPair); await() .pollInterval(1, SECONDS) diff --git a/acme4j-it/src/test/java/org/shredzone/acme4j/it/pebble/OrderWildcardIT.java b/acme4j-it/src/test/java/org/shredzone/acme4j/it/pebble/OrderWildcardIT.java index 8fd2f972..79f2298c 100644 --- a/acme4j-it/src/test/java/org/shredzone/acme4j/it/pebble/OrderWildcardIT.java +++ b/acme4j-it/src/test/java/org/shredzone/acme4j/it/pebble/OrderWildcardIT.java @@ -28,7 +28,6 @@ import org.shredzone.acme4j.AccountBuilder; import org.shredzone.acme4j.Session; import org.shredzone.acme4j.Status; import org.shredzone.acme4j.challenge.Dns01Challenge; -import org.shredzone.acme4j.util.CSRBuilder; /** * Tests a complete wildcard certificate order. Wildcard certificates currently only @@ -95,13 +94,7 @@ public class OrderWildcardIT extends PebbleITBase { assertThat(auth.getStatus()).isEqualTo(Status.VALID); } - var csr = new CSRBuilder(); - csr.addDomain(TEST_DOMAIN); - csr.addDomain(TEST_WILDCARD_DOMAIN); - csr.sign(domainKeyPair); - var encodedCsr = csr.getEncoded(); - - order.execute(encodedCsr); + order.execute(domainKeyPair); await() .pollInterval(1, SECONDS) @@ -115,7 +108,10 @@ public class OrderWildcardIT extends PebbleITBase { assertThat(cert).isNotNull(); assertThat(cert.getNotAfter()).isNotEqualTo(notBefore); assertThat(cert.getNotBefore()).isNotEqualTo(notAfter); - assertThat(cert.getSubjectX500Principal().getName()).contains("CN=" + TEST_DOMAIN); + assertThat(cert.getSubjectX500Principal().getName()).satisfiesAnyOf( + name -> assertThat(name).contains("CN=" + TEST_DOMAIN), + name -> assertThat(name).contains("CN=" + TEST_WILDCARD_DOMAIN) + ); var san = cert.getSubjectAlternativeNames().stream() .filter(it -> ((Number) it.get(0)).intValue() == GeneralName.dNSName) diff --git a/src/doc/docs/usage/order.md b/src/doc/docs/usage/order.md index 2572b965..a52d1c91 100644 --- a/src/doc/docs/usage/order.md +++ b/src/doc/docs/usage/order.md @@ -75,13 +75,40 @@ The response you have set up before is not needed any more. You can (and should) ## Finalize the Order -After successfully completing all authorizations, the order needs to be finalized by providing 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. +After successfully completing all authorizations, the order needs to be finalized. -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. There is a [`CSRBuilder`](../acme4j-client/apidocs/org.shredzone.acme4j.utils/org/shredzone/acme4j/util/CSRBuilder.html) for your convenience. You can also use [`KeyPairUtils`](../acme4j-client/apidocs/org.shredzone.acme4j.utils/org/shredzone/acme4j/util/KeyPairUtils.html) for generating a new key pair for your domain. +First of all, you will need to generate a new key pair that is used for certification and encryption. You can use [`KeyPairUtils`](../acme4j-client/apidocs/org.shredzone.acme4j.utils/org/shredzone/acme4j/util/KeyPairUtils.html) for generating a new key pair for your domain. !!! tip Never use your account key pair as domain key pair, but always generate separate key pairs! +After that, the order can be finalized: + +```java +KeyPair domainKeyPair = ... // KeyPair to be used for HTTPS encryption + +order.execute(domainKeyPair); +``` + +_acme4j_ will automatically take care of creating a minimal CSR for this order. If you need to expand this CSR (e.g. with your company name), you can do so: + +```java +order.execute(domainKeyPair, csr -> { + csr.setOrganization("ACME Corp."); +}); +``` + +It depends on the CA if other CSR properties (like _Organization_, _Organization Unit_) are accepted. Some may even require these properties to be set, while others may ignore them when generating the certificate. + +!!! note + The correct technical term is _finalization_ of an order, according to RFC-8555. However, Java has a method called `Object.finalize()` which is problematic and should not be used. To avoid confusion with that method, the finalization methods are intentionally called `execute`. + +## Using CSR Files + +If you need more control over the CSR file, you can also provide a PKCS#10 CSR file, either as `PKCS10CertificationRequest` instance or as DER formatted binary. 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. + +You can use command like tools like `openssl` or Java frameworks like [Bouncy Castle](http://www.bouncycastle.org/java.html) for generating the CSR file. There is also a [`CSRBuilder`](../acme4j-client/apidocs/org.shredzone.acme4j.utils/org/shredzone/acme4j/util/CSRBuilder.html) for your convenience. + ```java KeyPair domainKeyPair = ... // KeyPair to be used for HTTPS encryption @@ -94,13 +121,13 @@ 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: +Optionally the CSR can be written as a file, so you can use it again (e.g. for renewal of the certificate). ```java csrb.write(new FileWriter("example.csr")); ``` -After that, finalize the order: +After that, finalize the order by providing the CSR: ```java order.execute(csr);