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 acd17e81..713a20b2 100644 --- a/acme4j-example/src/main/java/org/shredzone/acme4j/ClientTest.java +++ b/acme4j-example/src/main/java/org/shredzone/acme4j/ClientTest.java @@ -50,6 +50,7 @@ public class ClientTest { private static final File DOMAIN_KEY_FILE = new File("domain.key"); private static final File DOMAIN_CERT_FILE = new File("domain.crt"); private static final File CERT_CHAIN_FILE = new File("chain.crt"); + private static final File DOMAIN_CHAIN_FILE = new File("domain-chain.crt"); private static final File DOMAIN_CSR_FILE = new File("domain.csr"); private static final int KEY_SIZE = 2048; @@ -181,14 +182,21 @@ public class ClientTest { // Download the certificate X509Certificate cert = certificate.download(); + X509Certificate[] chain = certificate.downloadChain(); + + // Write certificate only (e.g. for Apache's SSLCertificateFile) try (FileWriter fw = new FileWriter(DOMAIN_CERT_FILE)) { CertificateUtils.writeX509Certificate(cert, fw); } - // Download the certificate chain - X509Certificate[] chain = certificate.downloadChain(); + // Write chain only (e.g. for Apache's SSLCertificateChainFile) try (FileWriter fw = new FileWriter(CERT_CHAIN_FILE)) { - CertificateUtils.writeX509CertificateChain(chain, fw); + CertificateUtils.writeX509CertificateChain(fw, null, chain); + } + + // Write combined certificate and chain (e.g. for nginx) + try (FileWriter fw = new FileWriter(DOMAIN_CHAIN_FILE)) { + CertificateUtils.writeX509CertificateChain(fw, cert, chain); } // Revoke the certificate (uncomment if needed...) diff --git a/acme4j-utils/src/main/java/org/shredzone/acme4j/util/CSRBuilder.java b/acme4j-utils/src/main/java/org/shredzone/acme4j/util/CSRBuilder.java index 05de5777..fdc83232 100644 --- a/acme4j-utils/src/main/java/org/shredzone/acme4j/util/CSRBuilder.java +++ b/acme4j-utils/src/main/java/org/shredzone/acme4j/util/CSRBuilder.java @@ -44,7 +44,7 @@ import org.bouncycastle.util.io.pem.PemWriter; /** * Generator for a CSR (Certificate Signing Request) suitable for ACME servers. *

- * Requires {@code Bouncy Castle}. + * Requires {@code Bouncy Castle}. This class is part of the {@code acme4j-utils} module. */ public class CSRBuilder { private static final String SIGNATURE_ALG = "SHA256withRSA"; @@ -199,7 +199,8 @@ public class CSRBuilder { * Writes the signed certificate request to a {@link Writer}. * * @param w - * {@link Writer} to write the PEM file to + * {@link Writer} to write the PEM file to. The {@link Writer} is closed + * after use. */ public void write(Writer w) throws IOException { if (csr == null) { @@ -215,7 +216,8 @@ public class CSRBuilder { * Writes the signed certificate request to an {@link OutputStream}. * * @param out - * {@link OutputStream} to write the PEM file to + * {@link OutputStream} to write the PEM file to. The {@link OutputStream} + * is closed after use. */ public void write(OutputStream out) throws IOException { write(new OutputStreamWriter(out, "utf-8")); diff --git a/acme4j-utils/src/main/java/org/shredzone/acme4j/util/CertificateUtils.java b/acme4j-utils/src/main/java/org/shredzone/acme4j/util/CertificateUtils.java index 22029ca6..c8733719 100644 --- a/acme4j-utils/src/main/java/org/shredzone/acme4j/util/CertificateUtils.java +++ b/acme4j-utils/src/main/java/org/shredzone/acme4j/util/CertificateUtils.java @@ -42,7 +42,7 @@ import org.shredzone.acme4j.challenge.TlsSni02Challenge; /** * Utility class offering convenience methods for certificates. *

- * Requires {@code Bouncy Castle}. + * Requires {@code Bouncy Castle}. This class is part of the {@code acme4j-utils} module. */ public final class CertificateUtils { @@ -54,7 +54,8 @@ public final class CertificateUtils { * Reads an {@link X509Certificate} PEM file from an {@link InputStream}. * * @param in - * {@link InputStream} to read the certificate from. + * {@link InputStream} to read the certificate from. The + * {@link InputStream} is closed after use. * @return {@link X509Certificate} that was read */ public static X509Certificate readX509Certificate(InputStream in) throws IOException { @@ -72,7 +73,8 @@ public final class CertificateUtils { * @param cert * {@link X509Certificate} to write * @param out - * {@link OutputStream} to write the PEM file to + * {@link OutputStream} to write the PEM file to. The {@link OutputStream} + * is closed after use. */ public static void writeX509Certificate(X509Certificate cert, OutputStream out) throws IOException { writeX509Certificate(cert, new OutputStreamWriter(out, "utf-8")); @@ -84,7 +86,8 @@ public final class CertificateUtils { * @param cert * {@link X509Certificate} to write * @param w - * {@link Writer} to write the PEM file to + * {@link Writer} to write the PEM file to. The {@link Writer} is closed + * after use. */ public static void writeX509Certificate(X509Certificate cert, Writer w) throws IOException { try (JcaPEMWriter jw = new JcaPEMWriter(w)) { @@ -92,27 +95,56 @@ public final class CertificateUtils { } } + /** + * Writes a X.509 certificate chain to a PEM file. + * + * @param w + * {@link Writer} to write the certificate chain to. The {@link Writer} is + * closed after use. + * @param cert + * {@link X509Certificate} to write, {@code null} to skip this certificate + * @param chain + * {@link X509Certificate} chain to add to the certificate. {@code null} + * values are ignored, array may be empty. + */ + public static void writeX509CertificateChain(Writer w, X509Certificate cert, X509Certificate... chain) + throws IOException { + try (JcaPEMWriter jw = new JcaPEMWriter(w)) { + if (cert != null) { + jw.writeObject(cert); + } + if (chain != null) { + for (X509Certificate c : chain) { + if (c != null) { + jw.writeObject(c); + } + } + } + } + } + /** * Writes an X.509 certificate chain PEM file. * * @param chain * {@link X509Certificate[]} to write * @param w - * {@link Writer} to write the PEM file to + * {@link Writer} to write the PEM file to. The {@link Writer} is closed + * after use. + * @deprecated Use + * {@link #writeX509CertificateChain(Writer, X509Certificate, X509Certificate...)} */ + @Deprecated public static void writeX509CertificateChain(X509Certificate[] chain, Writer w) throws IOException { - try (JcaPEMWriter jw = new JcaPEMWriter(w)) { - for (X509Certificate cert : chain) { - jw.writeObject(cert); - } - } + writeX509CertificateChain(w, null, chain); } /** * Reads a CSR PEM file. * * @param in - * {@link InputStream} to read the CSR from. + * {@link InputStream} to read the CSR from. The {@link InputStream} is + * closed after use. * @return CSR that was read */ public static PKCS10CertificationRequest readCSR(InputStream in) throws IOException { diff --git a/acme4j-utils/src/main/java/org/shredzone/acme4j/util/KeyPairUtils.java b/acme4j-utils/src/main/java/org/shredzone/acme4j/util/KeyPairUtils.java index e2b44619..e9c0cb71 100644 --- a/acme4j-utils/src/main/java/org/shredzone/acme4j/util/KeyPairUtils.java +++ b/acme4j-utils/src/main/java/org/shredzone/acme4j/util/KeyPairUtils.java @@ -34,7 +34,7 @@ import org.bouncycastle.openssl.jcajce.JcaPEMWriter; /** * Utility class offering convenience methods for {@link KeyPair}. *

- * Requires {@code Bouncy Castle}. + * Requires {@code Bouncy Castle}. This class is part of the {@code acme4j-utils} module. */ public class KeyPairUtils { @@ -83,7 +83,8 @@ public class KeyPairUtils { * Reads a {@link KeyPair} from a PEM file. * * @param r - * {@link Reader} to read the PEM file from + * {@link Reader} to read the PEM file from. The {@link Reader} is closed + * after use. * @return {@link KeyPair} read */ public static KeyPair readKeyPair(Reader r) throws IOException { @@ -101,7 +102,8 @@ public class KeyPairUtils { * @param keypair * {@link KeyPair} to write * @param w - * {@link Writer} to write the PEM file to + * {@link Writer} to write the PEM file to. The {@link Writer} is closed + * after use. */ public static void writeKeyPair(KeyPair keypair, Writer w) throws IOException { try (JcaPEMWriter jw = new JcaPEMWriter(w)) { diff --git a/src/site/markdown/usage/certificate.md b/src/site/markdown/usage/certificate.md index f75fc9e6..711b961a 100644 --- a/src/site/markdown/usage/certificate.md +++ b/src/site/markdown/usage/certificate.md @@ -54,6 +54,29 @@ URI locationUri = ... // location URI from cert.getLocation() Certificate cert = Certificate.bind(session, locationUri); ``` +### Saving Certificates + +Most web servers, like _Apache_, _nginx_, but also other servers like _postfix_ or _dovecot_, need a combined certificate file that contains the leaf certificate itself, and the certificate chain up to the root certificate. `acme4j-utils` offers a method that helps to write the necessary file: + +```java +try (FileWriter fw = new FileWriter("cert-chain.crt")) { + CertificateUtils.writeX509CertificateChain(fw, cert, chain); +} +``` + +Some older servers may need the leaf certificate and the certificate chain in different files. Use this snippet to write both files: + +```java +try (FileWriter fw = new FileWriter("cert.pem")) { + CertificateUtils.writeX509Certificate(cert, fw); +} +try (FileWriter fw = new FileWriter("chain.pem")) { + CertificateUtils.writeX509CertificateChain(fw, null, chain); +} +``` + +These utility methods should be sufficient for most use cases. If you need the certificate written in a different format, see the [source code of `CertificateUtils`](https://github.com/shred/acme4j/blob/master/acme4j-utils/src/main/java/org/shredzone/acme4j/util/CertificateUtils.java) to find out how certificates are written using _Bouncy Castle_. + ### Multiple Domains The example above generates a certificate per domain. However, you would usually prefer to use a single certificate for multiple domains (for example, the domain itself and the `www.` subdomain).