From d5b4ff37dcb93797fd159dafe597f6dc41c6934f Mon Sep 17 00:00:00 2001 From: argy Date: Sun, 29 May 2016 03:44:27 +0300 Subject: [PATCH 1/2] add support for fetching certificate chain --- .../java/org/shredzone/acme4j/AcmeClient.java | 17 +++++++- .../org/shredzone/acme4j/CertificateURIs.java | 41 ++++++++++++++++++ .../acme4j/impl/AbstractAcmeClient.java | 42 +++++++++++++++---- .../acme4j/impl/AbstractAcmeClientTest.java | 16 +++++-- 4 files changed, 104 insertions(+), 12 deletions(-) create mode 100644 acme4j-client/src/main/java/org/shredzone/acme4j/CertificateURIs.java diff --git a/acme4j-client/src/main/java/org/shredzone/acme4j/AcmeClient.java b/acme4j-client/src/main/java/org/shredzone/acme4j/AcmeClient.java index de37367b..6a122903 100644 --- a/acme4j-client/src/main/java/org/shredzone/acme4j/AcmeClient.java +++ b/acme4j-client/src/main/java/org/shredzone/acme4j/AcmeClient.java @@ -135,9 +135,22 @@ public interface AcmeClient { * {@link Registration} to be used for conversation * @param csr * PKCS#10 Certificate Signing Request to be sent to the server - * @return {@link URI} the certificate can be downloaded from + * @return {@link CertificateURIs} the certificate and certificate chain can be downloaded from */ - URI requestCertificate(Registration registration, byte[] csr) throws AcmeException; + CertificateURIs requestCertificate(Registration registration, byte[] csr) throws AcmeException; + + /** + * Downloads chain for certificate. + * + * @param chainCertUri + * Certificate {@link URI} + * @return Downloaded {@link X509Certificate[]} + * + * @throws AcmeException + * if an {@link IOException} is thrown during certificate retrieval + * or the max recursion limit is exceeded + */ + X509Certificate[] downloadCertificateChain(URI chainCertUri) throws AcmeException; /** * Downloads a certificate. diff --git a/acme4j-client/src/main/java/org/shredzone/acme4j/CertificateURIs.java b/acme4j-client/src/main/java/org/shredzone/acme4j/CertificateURIs.java new file mode 100644 index 00000000..2ccd2e17 --- /dev/null +++ b/acme4j-client/src/main/java/org/shredzone/acme4j/CertificateURIs.java @@ -0,0 +1,41 @@ +package org.shredzone.acme4j; + +import java.net.URI; + +/** + * Represents the URIs returned by a certificate request + * + * @author cargy + * + */ +public class CertificateURIs { + + private final URI certUri; + private final URI chainCertUri; + + public CertificateURIs(URI certUri, URI chainCertUri) { + this.certUri = certUri; + this.chainCertUri = chainCertUri; + } + + /** + * The URI from which the client may fetch the certificate + * + * @return + * {@link URI} the certificate can be downloaded from + */ + public URI getCertUri() { + return certUri; + } + + /** + * The URI from which the client may fetch a chain of CA certificates + * + * @return + * {@link URI} the certificate chain can be downloaded from + */ + public URI getChainCertUri() { + return chainCertUri; + } + +} diff --git a/acme4j-client/src/main/java/org/shredzone/acme4j/impl/AbstractAcmeClient.java b/acme4j-client/src/main/java/org/shredzone/acme4j/impl/AbstractAcmeClient.java index f4084c98..0cd7bd81 100644 --- a/acme4j-client/src/main/java/org/shredzone/acme4j/impl/AbstractAcmeClient.java +++ b/acme4j-client/src/main/java/org/shredzone/acme4j/impl/AbstractAcmeClient.java @@ -28,10 +28,7 @@ import java.util.Map; import org.jose4j.jwk.PublicJsonWebKey; import org.jose4j.jws.JsonWebSignature; import org.jose4j.lang.JoseException; -import org.shredzone.acme4j.AcmeClient; -import org.shredzone.acme4j.Authorization; -import org.shredzone.acme4j.Registration; -import org.shredzone.acme4j.Status; +import org.shredzone.acme4j.*; import org.shredzone.acme4j.challenge.Challenge; import org.shredzone.acme4j.connector.Connection; import org.shredzone.acme4j.connector.Resource; @@ -54,6 +51,7 @@ import org.slf4j.LoggerFactory; */ public abstract class AbstractAcmeClient implements AcmeClient { private static final Logger LOG = LoggerFactory.getLogger(AbstractAcmeClient.class); + private static final int MAX_CHAIN_LENGTH = 10; private final Session session = new Session(); @@ -410,7 +408,7 @@ public abstract class AbstractAcmeClient implements AcmeClient { } @Override - public URI requestCertificate(Registration registration, byte[] csr) throws AcmeException { + public CertificateURIs requestCertificate(Registration registration, byte[] csr) throws AcmeException { if (registration == null) { throw new NullPointerException("registration must not be null"); } @@ -433,12 +431,42 @@ public abstract class AbstractAcmeClient implements AcmeClient { // Optionally returns the certificate. Currently it is just ignored. // X509Certificate cert = conn.readCertificate(); - - return conn.getLocation(); + + return new CertificateURIs(conn.getLocation(), conn.getLink("up")); } catch (IOException ex) { throw new AcmeNetworkException(ex); } } + + @Override + public X509Certificate[] downloadCertificateChain(URI chainCertUri) throws AcmeException { + if (chainCertUri == null) { + throw new NullPointerException("certChainUri must not be null"); + } + + LOG.debug("getCertificateChain"); + + List certChain = new ArrayList<>(); + URI link = chainCertUri; + while (link != null && certChain.size() < MAX_CHAIN_LENGTH) { + try (Connection conn = createConnection()) { + int rc = conn.sendRequest(chainCertUri); + if (rc != HttpURLConnection.HTTP_OK) { + conn.throwAcmeException(); + } + + certChain.add(conn.readCertificate()); + link = conn.getLink("up"); + } catch (IOException ex) { + throw new AcmeNetworkException(ex); + } + } + + if (link != null) + throw new AcmeException("Recursion limit reached (" + MAX_CHAIN_LENGTH + "). Didn't get " + link); + + return certChain.toArray(new X509Certificate[certChain.size()]); + } @Override public X509Certificate downloadCertificate(URI certUri) throws AcmeException { diff --git a/acme4j-client/src/test/java/org/shredzone/acme4j/impl/AbstractAcmeClientTest.java b/acme4j-client/src/test/java/org/shredzone/acme4j/impl/AbstractAcmeClientTest.java index 30dfdc98..e2898891 100644 --- a/acme4j-client/src/test/java/org/shredzone/acme4j/impl/AbstractAcmeClientTest.java +++ b/acme4j-client/src/test/java/org/shredzone/acme4j/impl/AbstractAcmeClientTest.java @@ -32,6 +32,7 @@ import org.jose4j.lang.JoseException; import org.junit.Before; import org.junit.Test; import org.shredzone.acme4j.Authorization; +import org.shredzone.acme4j.CertificateURIs; import org.shredzone.acme4j.Registration; import org.shredzone.acme4j.Status; import org.shredzone.acme4j.challenge.Challenge; @@ -56,6 +57,7 @@ public class AbstractAcmeClientTest { private URI resourceUri; private URI locationUri; + private URI certChainUri; private URI agreementUri; private KeyPair accountKeyPair; private Registration testRegistration; @@ -64,6 +66,7 @@ public class AbstractAcmeClientTest { public void setup() throws IOException, URISyntaxException { resourceUri = new URI("https://example.com/acme/some-resource"); locationUri = new URI("https://example.com/acme/some-location"); + certChainUri = new URI("https://example.com/acme/some-url"); agreementUri = new URI("http://example.com/agreement.pdf"); accountKeyPair = TestUtils.createKeyPair(); testRegistration = new Registration(accountKeyPair); @@ -456,7 +459,8 @@ public class AbstractAcmeClientTest { @Test public void testRequestCertificate() throws AcmeException, IOException { Connection connection = new DummyConnection() { - @Override + + @Override public int sendSignedRequest(URI uri, ClaimBuilder claims, Session session, Registration registration) { assertThat(uri, is(resourceUri)); assertThat(claims.toString(), sameJSONAs(getJson("requestCertificateRequest"))); @@ -469,15 +473,21 @@ public class AbstractAcmeClientTest { public URI getLocation() { return locationUri; } + + @Override + public URI getLink(String relation) { + return certChainUri; + } }; TestableAbstractAcmeClient client = new TestableAbstractAcmeClient(connection); client.putTestResource(Resource.NEW_CERT, resourceUri); byte[] csr = TestUtils.getResourceAsByteArray("/csr.der"); - URI certUri = client.requestCertificate(testRegistration, csr); + CertificateURIs certUris = client.requestCertificate(testRegistration, csr); - assertThat(certUri, is(locationUri)); + assertThat(certUris.getCertUri(), is(locationUri)); + assertThat(certUris.getChainCertUri(), is(certChainUri)); } /** From b13c90b7a4e3478e2e7f0d7e3f1dc26f67801bff Mon Sep 17 00:00:00 2001 From: argy Date: Thu, 2 Jun 2016 23:24:44 +0300 Subject: [PATCH 2/2] fixed code formatting issues and added copyright header --- .../org/shredzone/acme4j/CertificateURIs.java | 95 +++++++++++-------- .../acme4j/impl/AbstractAcmeClient.java | 53 ++++++----- .../acme4j/impl/AbstractAcmeClientTest.java | 5 +- .../java/org/shredzone/acme4j/ClientTest.java | 15 ++- .../acme4j/util/CertificateUtils.java | 15 +++ 5 files changed, 110 insertions(+), 73 deletions(-) diff --git a/acme4j-client/src/main/java/org/shredzone/acme4j/CertificateURIs.java b/acme4j-client/src/main/java/org/shredzone/acme4j/CertificateURIs.java index 2ccd2e17..886e3f35 100644 --- a/acme4j-client/src/main/java/org/shredzone/acme4j/CertificateURIs.java +++ b/acme4j-client/src/main/java/org/shredzone/acme4j/CertificateURIs.java @@ -1,41 +1,54 @@ -package org.shredzone.acme4j; - -import java.net.URI; - -/** - * Represents the URIs returned by a certificate request - * - * @author cargy - * - */ -public class CertificateURIs { - - private final URI certUri; - private final URI chainCertUri; - - public CertificateURIs(URI certUri, URI chainCertUri) { - this.certUri = certUri; - this.chainCertUri = chainCertUri; - } - - /** - * The URI from which the client may fetch the certificate - * - * @return - * {@link URI} the certificate can be downloaded from - */ - public URI getCertUri() { - return certUri; - } - - /** - * The URI from which the client may fetch a chain of CA certificates - * - * @return - * {@link URI} the certificate chain can be downloaded from - */ - public URI getChainCertUri() { - return chainCertUri; - } - -} +/* + * acme4j - Java ACME client + * + * Copyright (C) 2015 Richard "Shred" Körber + * http://acme4j.shredzone.org + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + */ +package org.shredzone.acme4j; + +import java.net.URI; + +/** + * Represents the URIs returned by a certificate request + * + * @author cargy + * + */ +public class CertificateURIs { + + private final URI certUri; + private final URI chainCertUri; + + public CertificateURIs(URI certUri, URI chainCertUri) { + this.certUri = certUri; + this.chainCertUri = chainCertUri; + } + + /** + * The URI from which the client may fetch the certificate + * + * @return + * {@link URI} the certificate can be downloaded from + */ + public URI getCertUri() { + return certUri; + } + + /** + * The URI from which the client may fetch a chain of CA certificates + * + * @return + * {@link URI} the certificate chain can be downloaded from + */ + public URI getChainCertUri() { + return chainCertUri; + } + +} diff --git a/acme4j-client/src/main/java/org/shredzone/acme4j/impl/AbstractAcmeClient.java b/acme4j-client/src/main/java/org/shredzone/acme4j/impl/AbstractAcmeClient.java index 0cd7bd81..a7533e47 100644 --- a/acme4j-client/src/main/java/org/shredzone/acme4j/impl/AbstractAcmeClient.java +++ b/acme4j-client/src/main/java/org/shredzone/acme4j/impl/AbstractAcmeClient.java @@ -28,7 +28,11 @@ import java.util.Map; import org.jose4j.jwk.PublicJsonWebKey; import org.jose4j.jws.JsonWebSignature; import org.jose4j.lang.JoseException; -import org.shredzone.acme4j.*; +import org.shredzone.acme4j.AcmeClient; +import org.shredzone.acme4j.Authorization; +import org.shredzone.acme4j.CertificateURIs; +import org.shredzone.acme4j.Registration; +import org.shredzone.acme4j.Status; import org.shredzone.acme4j.challenge.Challenge; import org.shredzone.acme4j.connector.Connection; import org.shredzone.acme4j.connector.Resource; @@ -431,41 +435,40 @@ public abstract class AbstractAcmeClient implements AcmeClient { // Optionally returns the certificate. Currently it is just ignored. // X509Certificate cert = conn.readCertificate(); - + return new CertificateURIs(conn.getLocation(), conn.getLink("up")); } catch (IOException ex) { throw new AcmeNetworkException(ex); } } - + @Override public X509Certificate[] downloadCertificateChain(URI chainCertUri) throws AcmeException { - if (chainCertUri == null) { - throw new NullPointerException("certChainUri must not be null"); - } + if (chainCertUri == null) { + throw new NullPointerException("certChainUri must not be null"); + } - LOG.debug("getCertificateChain"); + LOG.debug("getCertificateChain"); - List certChain = new ArrayList<>(); - URI link = chainCertUri; - while (link != null && certChain.size() < MAX_CHAIN_LENGTH) { - try (Connection conn = createConnection()) { - int rc = conn.sendRequest(chainCertUri); - if (rc != HttpURLConnection.HTTP_OK) { - conn.throwAcmeException(); - } + List certChain = new ArrayList<>(); + URI link = chainCertUri; + while (link != null && certChain.size() < MAX_CHAIN_LENGTH) { + try (Connection conn = createConnection()) { + int rc = conn.sendRequest(chainCertUri); + if (rc != HttpURLConnection.HTTP_OK) { + conn.throwAcmeException(); + } - certChain.add(conn.readCertificate()); - link = conn.getLink("up"); - } catch (IOException ex) { - throw new AcmeNetworkException(ex); - } - } - - if (link != null) - throw new AcmeException("Recursion limit reached (" + MAX_CHAIN_LENGTH + "). Didn't get " + link); + certChain.add(conn.readCertificate()); + link = conn.getLink("up"); + } catch (IOException ex) { + throw new AcmeNetworkException(ex); + } + } + if (link != null) + throw new AcmeException("Recursion limit reached (" + MAX_CHAIN_LENGTH + "). Didn't get " + link); - return certChain.toArray(new X509Certificate[certChain.size()]); + return certChain.toArray(new X509Certificate[certChain.size()]); } @Override diff --git a/acme4j-client/src/test/java/org/shredzone/acme4j/impl/AbstractAcmeClientTest.java b/acme4j-client/src/test/java/org/shredzone/acme4j/impl/AbstractAcmeClientTest.java index e2898891..7f48b610 100644 --- a/acme4j-client/src/test/java/org/shredzone/acme4j/impl/AbstractAcmeClientTest.java +++ b/acme4j-client/src/test/java/org/shredzone/acme4j/impl/AbstractAcmeClientTest.java @@ -459,8 +459,7 @@ public class AbstractAcmeClientTest { @Test public void testRequestCertificate() throws AcmeException, IOException { Connection connection = new DummyConnection() { - - @Override + @Override public int sendSignedRequest(URI uri, ClaimBuilder claims, Session session, Registration registration) { assertThat(uri, is(resourceUri)); assertThat(claims.toString(), sameJSONAs(getJson("requestCertificateRequest"))); @@ -473,7 +472,7 @@ public class AbstractAcmeClientTest { public URI getLocation() { return locationUri; } - + @Override public URI getLink(String relation) { return certChainUri; 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 b1dc70a9..d174ef44 100644 --- a/acme4j-example/src/main/java/org/shredzone/acme4j/ClientTest.java +++ b/acme4j-example/src/main/java/org/shredzone/acme4j/ClientTest.java @@ -18,7 +18,6 @@ import java.io.FileReader; import java.io.FileWriter; import java.io.IOException; import java.io.Writer; -import java.net.URI; import java.security.KeyPair; import java.security.Security; import java.security.cert.X509Certificate; @@ -51,6 +50,7 @@ public class ClientTest { private static final File USER_KEY_FILE = new File("user.key"); 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_CSR_FILE = new File("domain.csr"); private static final int KEY_SIZE = 2048; @@ -176,16 +176,23 @@ public class ClientTest { } // Request a signed certificate - URI certificateUri = client.requestCertificate(reg, csrb.getEncoded()); + CertificateURIs certificateUris = client.requestCertificate(reg, csrb.getEncoded()); LOG.info("Success! The certificate for domains " + domains + " has been generated!"); - LOG.info("Certificate URI: " + certificateUri); + LOG.info("Certificate URI: " + certificateUris.getCertUri()); + LOG.info("Certificate Chain URI: " + certificateUris.getChainCertUri()); // Download the certificate - X509Certificate cert = client.downloadCertificate(certificateUri); + X509Certificate cert = client.downloadCertificate(certificateUris.getCertUri()); try (FileWriter fw = new FileWriter(DOMAIN_CERT_FILE)) { CertificateUtils.writeX509Certificate(cert, fw); } + // Download the certificate chain + X509Certificate[] chain = client.downloadCertificateChain(certificateUris.getChainCertUri()); + try (FileWriter fw = new FileWriter(CERT_CHAIN_FILE)) { + CertificateUtils.writeX509CertificateChain(chain, fw); + } + // Revoke the certificate (uncomment if needed...) // client.revokeCertificate(reg, cert); } 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 b50e9214..54af90fe 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 @@ -94,6 +94,21 @@ public final class CertificateUtils { } } + /** + * Writes an X.509 certificate chain PEM file. + * + * @param chain + * {@link X509Certificate[]} to write + * @param w + * {@link Writer} to write the PEM file to + */ + public static void writeX509CertificateChain(X509Certificate[] chain, Writer w) throws IOException { + try (JcaPEMWriter jw = new JcaPEMWriter(w)) { + for (X509Certificate cert : chain) + jw.writeObject(cert); + } + } + /** * Reads a CSR PEM file. *