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
+ * 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);