From 69fbe81e2d00beaf854661f823598ba0b831e532 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Richard=20K=C3=B6rber?= Date: Sat, 7 Jul 2018 17:12:19 +0200 Subject: [PATCH] Add method to download full cert and chain --- .../org/shredzone/acme4j/Certificate.java | 34 +++++++++++++++++-- .../org/shredzone/acme4j/CertificateTest.java | 24 ++++++++++--- .../shredzone/acme4j/toolbox/TestUtils.java | 15 ++++++++ acme4j-client/src/test/resources/issuer.pem | 27 +++++++++++++++ src/site/markdown/usage/certificate.md | 13 +++++++ 5 files changed, 107 insertions(+), 6 deletions(-) create mode 100644 acme4j-client/src/test/resources/issuer.pem diff --git a/acme4j-client/src/main/java/org/shredzone/acme4j/Certificate.java b/acme4j-client/src/main/java/org/shredzone/acme4j/Certificate.java index 9cf1d469..be06aa34 100644 --- a/acme4j-client/src/main/java/org/shredzone/acme4j/Certificate.java +++ b/acme4j-client/src/main/java/org/shredzone/acme4j/Certificate.java @@ -77,7 +77,10 @@ public class Certificate extends AcmeResource { } /** - * Downloads the certificate. The result is cached. + * Downloads the certificate. The result is only the end-entity certificate, without + * the issuer chain. + *

+ * The result is cached. * * @return {@link X509Certificate} that was downloaded * @throws AcmeRetryAfterException @@ -85,6 +88,7 @@ public class Certificate extends AcmeResource { * estimated date when it will be ready for download. You should wait for * the date given in {@link AcmeRetryAfterException#getRetryAfter()} * before trying again. + * @see #downloadFullChain() */ public X509Certificate download() throws AcmeException { if (cert == null) { @@ -102,7 +106,9 @@ public class Certificate extends AcmeResource { } /** - * Downloads the certificate chain. The result is cached. + * Downloads the issuer chain. The result does not contain the end-entity certificate. + *

+ * The result is cached. * * @return Chain of {@link X509Certificate}s * @throws AcmeRetryAfterException @@ -144,6 +150,30 @@ public class Certificate extends AcmeResource { return Arrays.copyOf(chain, chain.length); } + /** + * Downloads the certificate and the corresponding issuer chain. The issued + * certificate is always on index 0, followed by the CA certificates, with the root CA + * being at the last index. + *

+ * The result is cached. + * + * @return Chain of {@link X509Certificate}s + * @throws AcmeRetryAfterException + * the certificate is still being created, and the server returned an + * estimated date when it will be ready for download. You should wait for + * the date given in {@link AcmeRetryAfterException#getRetryAfter()} + * before trying again. + * @since 1.1 + */ + public X509Certificate[] downloadFullChain() throws AcmeException { + downloadChain(); + + X509Certificate[] result = new X509Certificate[chain.length + 1]; + result[0] = cert; + System.arraycopy(chain, 0, result, 1, chain.length); + return result; + } + /** * Revokes this certificate. */ diff --git a/acme4j-client/src/test/java/org/shredzone/acme4j/CertificateTest.java b/acme4j-client/src/test/java/org/shredzone/acme4j/CertificateTest.java index e9fddd4f..aed9bf00 100644 --- a/acme4j-client/src/test/java/org/shredzone/acme4j/CertificateTest.java +++ b/acme4j-client/src/test/java/org/shredzone/acme4j/CertificateTest.java @@ -49,6 +49,7 @@ public class CertificateTest { @Test public void testDownload() throws AcmeException, IOException { final X509Certificate originalCert = TestUtils.createCertificate(); + final X509Certificate issuerCert = TestUtils.createIssuerCertificate(); TestableConnectionProvider provider = new TestableConnectionProvider() { private boolean isLocationUrl; @@ -75,7 +76,11 @@ public class CertificateTest { @Override public X509Certificate readCertificate() { - return originalCert; + if (isLocationUrl) { + return originalCert; + } else { + return issuerCert; + } } @Override @@ -99,12 +104,23 @@ public class CertificateTest { X509Certificate[] downloadedChain = cert.downloadChain(); assertThat(downloadedChain.length, is(1)); - assertThat(downloadedChain[0], is(sameInstance(originalCert))); + assertThat(downloadedChain[0], is(sameInstance(issuerCert))); - // Make sure the chain array is a local copy + X509Certificate[] downloadedFullChain = cert.downloadFullChain(); + assertThat(downloadedFullChain.length, is(2)); + assertThat(downloadedFullChain[0], is(sameInstance(originalCert))); + assertThat(downloadedFullChain[1], is(sameInstance(issuerCert))); + + // Make sure the chain arrays are a local copy downloadedChain[0] = null; X509Certificate[] downloadedChain2 = cert.downloadChain(); - assertThat(downloadedChain2[0], is(sameInstance(originalCert))); + assertThat(downloadedChain2[0], is(sameInstance(issuerCert))); + + downloadedFullChain[0] = null; + downloadedFullChain[1] = null; + X509Certificate[] downloadedFullChain2 = cert.downloadFullChain(); + assertThat(downloadedFullChain2[0], is(sameInstance(originalCert))); + assertThat(downloadedFullChain2[1], is(sameInstance(issuerCert))); provider.close(); } diff --git a/acme4j-client/src/test/java/org/shredzone/acme4j/toolbox/TestUtils.java b/acme4j-client/src/test/java/org/shredzone/acme4j/toolbox/TestUtils.java index e1e00ad3..b6312314 100644 --- a/acme4j-client/src/test/java/org/shredzone/acme4j/toolbox/TestUtils.java +++ b/acme4j-client/src/test/java/org/shredzone/acme4j/toolbox/TestUtils.java @@ -241,6 +241,21 @@ public final class TestUtils { } } + /** + * Creates the issuer certificate for testing. This certificate is read from a test + * resource and is guaranteed not to change between test runs. + * + * @return {@link X509Certificate} for testing + */ + public static X509Certificate createIssuerCertificate() throws IOException { + try (InputStream cert = TestUtils.class.getResourceAsStream("/issuer.pem")) { + CertificateFactory certificateFactory = CertificateFactory.getInstance("X.509"); + return (X509Certificate) certificateFactory.generateCertificate(cert); + } catch (CertificateException ex) { + throw new IOException(ex); + } + } + /** * Creates a matcher that matches an array of int primitives. The array must contain * exactly all of the given values, in any order. diff --git a/acme4j-client/src/test/resources/issuer.pem b/acme4j-client/src/test/resources/issuer.pem new file mode 100644 index 00000000..0002462c --- /dev/null +++ b/acme4j-client/src/test/resources/issuer.pem @@ -0,0 +1,27 @@ +-----BEGIN CERTIFICATE----- +MIIEkjCCA3qgAwIBAgIQCgFBQgAAAVOFc2oLheynCDANBgkqhkiG9w0BAQsFADA/ +MSQwIgYDVQQKExtEaWdpdGFsIFNpZ25hdHVyZSBUcnVzdCBDby4xFzAVBgNVBAMT +DkRTVCBSb290IENBIFgzMB4XDTE2MDMxNzE2NDA0NloXDTIxMDMxNzE2NDA0Nlow +SjELMAkGA1UEBhMCVVMxFjAUBgNVBAoTDUxldCdzIEVuY3J5cHQxIzAhBgNVBAMT +GkxldCdzIEVuY3J5cHQgQXV0aG9yaXR5IFgzMIIBIjANBgkqhkiG9w0BAQEFAAOC +AQ8AMIIBCgKCAQEAnNMM8FrlLke3cl03g7NoYzDq1zUmGSXhvb418XCSL7e4S0EF +q6meNQhY7LEqxGiHC6PjdeTm86dicbp5gWAf15Gan/PQeGdxyGkOlZHP/uaZ6WA8 +SMx+yk13EiSdRxta67nsHjcAHJyse6cF6s5K671B5TaYucv9bTyWaN8jKkKQDIZ0 +Z8h/pZq4UmEUEz9l6YKHy9v6Dlb2honzhT+Xhq+w3Brvaw2VFn3EK6BlspkENnWA +a6xK8xuQSXgvopZPKiAlKQTGdMDQMc2PMTiVFrqoM7hD8bEfwzB/onkxEz0tNvjj +/PIzark5McWvxI0NHWQWM6r6hCm21AvA2H3DkwIDAQABo4IBfTCCAXkwEgYDVR0T +AQH/BAgwBgEB/wIBADAOBgNVHQ8BAf8EBAMCAYYwfwYIKwYBBQUHAQEEczBxMDIG +CCsGAQUFBzABhiZodHRwOi8vaXNyZy50cnVzdGlkLm9jc3AuaWRlbnRydXN0LmNv +bTA7BggrBgEFBQcwAoYvaHR0cDovL2FwcHMuaWRlbnRydXN0LmNvbS9yb290cy9k +c3Ryb290Y2F4My5wN2MwHwYDVR0jBBgwFoAUxKexpHsscfrb4UuQdf/EFWCFiRAw +VAYDVR0gBE0wSzAIBgZngQwBAgEwPwYLKwYBBAGC3xMBAQEwMDAuBggrBgEFBQcC +ARYiaHR0cDovL2Nwcy5yb290LXgxLmxldHNlbmNyeXB0Lm9yZzA8BgNVHR8ENTAz +MDGgL6AthitodHRwOi8vY3JsLmlkZW50cnVzdC5jb20vRFNUUk9PVENBWDNDUkwu +Y3JsMB0GA1UdDgQWBBSoSmpjBH3duubRObemRWXv86jsoTANBgkqhkiG9w0BAQsF +AAOCAQEA3TPXEfNjWDjdGBX7CVW+dla5cEilaUcne8IkCJLxWh9KEik3JHRRHGJo +uM2VcGfl96S8TihRzZvoroed6ti6WqEBmtzw3Wodatg+VyOeph4EYpr/1wXKtx8/ +wApIvJSwtmVi4MFU5aMqrSDE6ea73Mj2tcMyo5jMd6jmeWUHK8so/joWUoHOUgwu +X4Po1QYz+3dszkDqMp4fklxBwXRsW10KXzPMTZ+sOPAveyxindmjkW8lGy+QsRlG +PfZ+G6Z6h7mjem0Y+iWlkYcV4PIWL1iwBi8saCbGS5jN2p8M+X+Q7UNKEkROb3N6 +KOqkqm57TH2H3eDJAkSnh6/DNFu0Qg== +-----END CERTIFICATE----- diff --git a/src/site/markdown/usage/certificate.md b/src/site/markdown/usage/certificate.md index c1c87183..da23c5ec 100644 --- a/src/site/markdown/usage/certificate.md +++ b/src/site/markdown/usage/certificate.md @@ -41,8 +41,13 @@ The `Certificate` object offers methods to download the certificate and the cert ```java X509Certificate cert = cert.download(); X509Certificate[] chain = cert.downloadChain(); +X509Certificate[] fullChain = cert.downloadFullChain(); ``` +* `cert` is the issued certificate. +* `chain` is the issuer chain that corresponds to `cert`. +* `fullChain` is the issued certificate (at index 0), followed by the issuer chain. + Congratulations! You have just created your first certificate via _acme4j_. `download()` may throw an `AcmeRetryAfterException`, giving an estimated time in `getRetryAfter()` for when the certificate is ready for download. You should then wait until that moment has been reached, before trying again. @@ -64,6 +69,14 @@ try (FileWriter fw = new FileWriter("cert-chain.crt")) { } ``` +Alternatively: + +```java +try (FileWriter fw = new FileWriter("cert-chain.crt")) { + CertificateUtils.writeX509CertificateChain(fw, null, fullChain); +} +``` + Some older servers may need the leaf certificate and the certificate chain in different files. Use this snippet to write both files: ```java