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.
*/