Automatically generate CSR in Order class

With this change, it is not stricly required anymore to create the CSR
oneself. The Order class contains all information to generate a basic
CSR itself.
pull/140/head
Richard Körber 2023-05-19 10:20:35 +02:00
parent e22b47f140
commit e8b83d6423
No known key found for this signature in database
GPG Key ID: AAB9FD19C78AA3E0
6 changed files with 122 additions and 45 deletions

View File

@ -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.
* <p>
* After a successful finalization, the certificate is available at
* If the finalization was successful, the certificate is provided via
* {@link #getCertificate()}.
* <p>
* 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 <em>not</em>
* 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)}).
* <p>
* 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<CSRBuilder> 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)}).
* <p>
* 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)}).
* <p>
* 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");

View File

@ -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 {

View File

@ -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)

View File

@ -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)

View File

@ -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)

View File

@ -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);