From 4c34f9afb527e947f71e8f7671c9516b47fa1c6b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Richard=20K=C3=B6rber?= Date: Tue, 2 May 2017 18:36:16 +0200 Subject: [PATCH] Add alternate certificates support --- .../org/shredzone/acme4j/Certificate.java | 30 +++++++++++++++++++ .../acme4j/connector/DefaultConnection.java | 20 +------------ .../org/shredzone/acme4j/util/AcmeUtils.java | 20 +++++++++++++ .../org/shredzone/acme4j/CertificateTest.java | 28 +++++++++++++++++ .../shredzone/acme4j/util/AcmeUtilsTest.java | 15 ++++++++++ 5 files changed, 94 insertions(+), 19 deletions(-) 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 76bfccaf..ec31923d 100644 --- a/acme4j-client/src/main/java/org/shredzone/acme4j/Certificate.java +++ b/acme4j-client/src/main/java/org/shredzone/acme4j/Certificate.java @@ -14,14 +14,18 @@ package org.shredzone.acme4j; import static java.util.Collections.unmodifiableList; +import static java.util.stream.Collectors.toCollection; import java.io.IOException; import java.io.Writer; import java.net.HttpURLConnection; +import java.net.URI; import java.net.URL; import java.security.cert.CertificateEncodingException; import java.security.cert.X509Certificate; import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; import java.util.List; import org.shredzone.acme4j.connector.Connection; @@ -36,12 +40,16 @@ import org.slf4j.LoggerFactory; /** * Represents a certificate and its certificate chain. + *

+ * Note that a certificate is immutable once it is issued. For renewal, a new certificate + * must be ordered. */ public class Certificate extends AcmeResource { private static final long serialVersionUID = 7381527770159084201L; private static final Logger LOG = LoggerFactory.getLogger(Certificate.class); private ArrayList certChain = null; + private ArrayList alternates = null; protected Certificate(Session session, URL certUrl) { super(session); @@ -73,6 +81,14 @@ public class Certificate extends AcmeResource { try (Connection conn = getSession().provider().connect()) { conn.sendRequest(getLocation(), getSession()); conn.accept(HttpURLConnection.HTTP_OK); + + Collection alternateList = conn.getLinks("alternate"); + if (alternateList != null) { + alternates = alternateList.stream() + .map(AcmeUtils::toURL) + .collect(toCollection(ArrayList::new)); + } + certChain = new ArrayList<>(conn.readCertificates()); } } @@ -100,6 +116,20 @@ public class Certificate extends AcmeResource { return unmodifiableList(certChain); } + /** + * Returns URLs to alternate certificate chains. + * + * @return Alternate certificate chains, or empty if there are none. + */ + public List getAlternates() { + lazyDownload(); + if (alternates != null) { + return unmodifiableList(alternates); + } else { + return Collections.emptyList(); + } + } + /** * Writes the certificate to the given writer. It is written in PEM format, with the * end-entity cert coming first, followed by the intermediate ceritificates. diff --git a/acme4j-client/src/main/java/org/shredzone/acme4j/connector/DefaultConnection.java b/acme4j-client/src/main/java/org/shredzone/acme4j/connector/DefaultConnection.java index 03100d04..b6b84dfb 100644 --- a/acme4j-client/src/main/java/org/shredzone/acme4j/connector/DefaultConnection.java +++ b/acme4j-client/src/main/java/org/shredzone/acme4j/connector/DefaultConnection.java @@ -14,13 +14,12 @@ package org.shredzone.acme4j.connector; import static java.util.stream.Collectors.toList; -import static org.shredzone.acme4j.util.AcmeUtils.keyAlgorithm; +import static org.shredzone.acme4j.util.AcmeUtils.*; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.net.HttpURLConnection; -import java.net.MalformedURLException; import java.net.URI; import java.net.URISyntaxException; import java.net.URL; @@ -496,21 +495,4 @@ public class DefaultConnection implements Connection { } } - /** - * Converts {@link URI} to {@link URL}. - * - * @param uri - * {@link URI} to convert - * @return {@link URL} - * @throws AcmeProtocolException - * if the URI could not be converted to URL - */ - private static URL toURL(URI uri) { - try { - return uri != null ? uri.toURL() : null; - } catch (MalformedURLException ex) { - throw new AcmeProtocolException("Invalid URL: " + uri, ex); - } - } - } diff --git a/acme4j-client/src/main/java/org/shredzone/acme4j/util/AcmeUtils.java b/acme4j-client/src/main/java/org/shredzone/acme4j/util/AcmeUtils.java index 8903802d..8c8d0d01 100644 --- a/acme4j-client/src/main/java/org/shredzone/acme4j/util/AcmeUtils.java +++ b/acme4j-client/src/main/java/org/shredzone/acme4j/util/AcmeUtils.java @@ -17,6 +17,9 @@ import java.io.IOException; import java.io.UnsupportedEncodingException; import java.io.Writer; import java.net.IDN; +import java.net.MalformedURLException; +import java.net.URI; +import java.net.URL; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.time.Instant; @@ -273,4 +276,21 @@ public final class AcmeUtils { out.append("\n-----END ").append(label.toString()).append("-----\n"); } + /** + * Converts {@link URI} to {@link URL}. + * + * @param uri + * {@link URI} to convert + * @return {@link URL} + * @throws AcmeProtocolException + * if the URI could not be converted to URL + */ + public static URL toURL(URI uri) { + try { + return uri != null ? uri.toURL() : null; + } catch (MalformedURLException ex) { + throw new AcmeProtocolException("Invalid URL: " + uri, ex); + } + } + } 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 d67a34cf..ac17390a 100644 --- a/acme4j-client/src/test/java/org/shredzone/acme4j/CertificateTest.java +++ b/acme4j-client/src/test/java/org/shredzone/acme4j/CertificateTest.java @@ -23,8 +23,11 @@ import java.io.IOException; import java.io.InputStream; import java.io.OutputStreamWriter; import java.net.HttpURLConnection; +import java.net.URI; import java.net.URL; import java.security.cert.X509Certificate; +import java.util.Arrays; +import java.util.Collection; import java.util.List; import org.junit.Test; @@ -66,6 +69,14 @@ public class CertificateTest { public List readCertificates() throws AcmeException { return originalCert; } + + @Override + public Collection getLinks(String relation) { + assertThat(relation, is("alternate")); + return Arrays.asList( + URI.create("https://example.com/acme/alt-cert/1"), + URI.create("https://example.com/acme/alt-cert/2")); + } }; Certificate cert = new Certificate(provider.createSession(), locationUrl); @@ -99,6 +110,11 @@ public class CertificateTest { } assertThat(writtenPem, is(originalPem)); + assertThat(cert.getAlternates(), is(notNullValue())); + assertThat(cert.getAlternates().size(), is(2)); + assertThat(cert.getAlternates().get(0), is(url("https://example.com/acme/alt-cert/1"))); + assertThat(cert.getAlternates().get(1), is(url("https://example.com/acme/alt-cert/2"))); + provider.close(); } @@ -138,6 +154,12 @@ public class CertificateTest { assertThat(certRequested, is(true)); return originalCert; } + + @Override + public Collection getLinks(String relation) { + assertThat(relation, is("alternate")); + return null; + } }; provider.putTestResource(Resource.REVOKE_CERT, resourceUrl); @@ -184,6 +206,12 @@ public class CertificateTest { assertThat(certRequested, is(true)); return originalCert; } + + @Override + public Collection getLinks(String relation) { + assertThat(relation, is("alternate")); + return null; + } }; provider.putTestResource(Resource.REVOKE_CERT, resourceUrl); diff --git a/acme4j-client/src/test/java/org/shredzone/acme4j/util/AcmeUtilsTest.java b/acme4j-client/src/test/java/org/shredzone/acme4j/util/AcmeUtilsTest.java index 4e55c9aa..8f20f8e5 100644 --- a/acme4j-client/src/test/java/org/shredzone/acme4j/util/AcmeUtilsTest.java +++ b/acme4j-client/src/test/java/org/shredzone/acme4j/util/AcmeUtilsTest.java @@ -24,6 +24,9 @@ import java.io.OutputStreamWriter; import java.io.Writer; import java.lang.reflect.Constructor; import java.lang.reflect.Modifier; +import java.net.MalformedURLException; +import java.net.URI; +import java.net.URL; import java.security.KeyPair; import java.security.Security; import java.security.cert.CertificateEncodingException; @@ -286,6 +289,18 @@ public class AcmeUtilsTest { assertThat(pemFile.toByteArray(), is(originalFile.toByteArray())); } + /** + * Test {@link AcmeUtils#toURL(URI)}. + */ + @Test + public void testToURL() throws MalformedURLException { + URI testUri = URI.create("https://example.com/foo/123"); + URL testUrl = testUri.toURL(); + + assertThat(AcmeUtils.toURL(testUri), is(testUrl)); + assertThat(AcmeUtils.toURL(null), is(nullValue())); + } + /** * Matches the given time. */