Add alternate certificates support

pull/55/head
Richard Körber 2017-05-02 18:36:16 +02:00
parent 7d83ef0e80
commit 4c34f9afb5
5 changed files with 94 additions and 19 deletions

View File

@ -14,14 +14,18 @@
package org.shredzone.acme4j; package org.shredzone.acme4j;
import static java.util.Collections.unmodifiableList; import static java.util.Collections.unmodifiableList;
import static java.util.stream.Collectors.toCollection;
import java.io.IOException; import java.io.IOException;
import java.io.Writer; import java.io.Writer;
import java.net.HttpURLConnection; import java.net.HttpURLConnection;
import java.net.URI;
import java.net.URL; import java.net.URL;
import java.security.cert.CertificateEncodingException; import java.security.cert.CertificateEncodingException;
import java.security.cert.X509Certificate; import java.security.cert.X509Certificate;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List; import java.util.List;
import org.shredzone.acme4j.connector.Connection; import org.shredzone.acme4j.connector.Connection;
@ -36,12 +40,16 @@ import org.slf4j.LoggerFactory;
/** /**
* Represents a certificate and its certificate chain. * Represents a certificate and its certificate chain.
* <p>
* Note that a certificate is immutable once it is issued. For renewal, a new certificate
* must be ordered.
*/ */
public class Certificate extends AcmeResource { public class Certificate extends AcmeResource {
private static final long serialVersionUID = 7381527770159084201L; private static final long serialVersionUID = 7381527770159084201L;
private static final Logger LOG = LoggerFactory.getLogger(Certificate.class); private static final Logger LOG = LoggerFactory.getLogger(Certificate.class);
private ArrayList<X509Certificate> certChain = null; private ArrayList<X509Certificate> certChain = null;
private ArrayList<URL> alternates = null;
protected Certificate(Session session, URL certUrl) { protected Certificate(Session session, URL certUrl) {
super(session); super(session);
@ -73,6 +81,14 @@ public class Certificate extends AcmeResource {
try (Connection conn = getSession().provider().connect()) { try (Connection conn = getSession().provider().connect()) {
conn.sendRequest(getLocation(), getSession()); conn.sendRequest(getLocation(), getSession());
conn.accept(HttpURLConnection.HTTP_OK); conn.accept(HttpURLConnection.HTTP_OK);
Collection<URI> alternateList = conn.getLinks("alternate");
if (alternateList != null) {
alternates = alternateList.stream()
.map(AcmeUtils::toURL)
.collect(toCollection(ArrayList::new));
}
certChain = new ArrayList<>(conn.readCertificates()); certChain = new ArrayList<>(conn.readCertificates());
} }
} }
@ -100,6 +116,20 @@ public class Certificate extends AcmeResource {
return unmodifiableList(certChain); return unmodifiableList(certChain);
} }
/**
* Returns URLs to alternate certificate chains.
*
* @return Alternate certificate chains, or empty if there are none.
*/
public List<URL> 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 * 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. * end-entity cert coming first, followed by the intermediate ceritificates.

View File

@ -14,13 +14,12 @@
package org.shredzone.acme4j.connector; package org.shredzone.acme4j.connector;
import static java.util.stream.Collectors.toList; 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.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.io.OutputStream; import java.io.OutputStream;
import java.net.HttpURLConnection; import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.URI; import java.net.URI;
import java.net.URISyntaxException; import java.net.URISyntaxException;
import java.net.URL; 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);
}
}
} }

View File

@ -17,6 +17,9 @@ import java.io.IOException;
import java.io.UnsupportedEncodingException; import java.io.UnsupportedEncodingException;
import java.io.Writer; import java.io.Writer;
import java.net.IDN; import java.net.IDN;
import java.net.MalformedURLException;
import java.net.URI;
import java.net.URL;
import java.security.MessageDigest; import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException; import java.security.NoSuchAlgorithmException;
import java.time.Instant; import java.time.Instant;
@ -273,4 +276,21 @@ public final class AcmeUtils {
out.append("\n-----END ").append(label.toString()).append("-----\n"); 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);
}
}
} }

View File

@ -23,8 +23,11 @@ import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.io.OutputStreamWriter; import java.io.OutputStreamWriter;
import java.net.HttpURLConnection; import java.net.HttpURLConnection;
import java.net.URI;
import java.net.URL; import java.net.URL;
import java.security.cert.X509Certificate; import java.security.cert.X509Certificate;
import java.util.Arrays;
import java.util.Collection;
import java.util.List; import java.util.List;
import org.junit.Test; import org.junit.Test;
@ -66,6 +69,14 @@ public class CertificateTest {
public List<X509Certificate> readCertificates() throws AcmeException { public List<X509Certificate> readCertificates() throws AcmeException {
return originalCert; return originalCert;
} }
@Override
public Collection<URI> 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); Certificate cert = new Certificate(provider.createSession(), locationUrl);
@ -99,6 +110,11 @@ public class CertificateTest {
} }
assertThat(writtenPem, is(originalPem)); 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(); provider.close();
} }
@ -138,6 +154,12 @@ public class CertificateTest {
assertThat(certRequested, is(true)); assertThat(certRequested, is(true));
return originalCert; return originalCert;
} }
@Override
public Collection<URI> getLinks(String relation) {
assertThat(relation, is("alternate"));
return null;
}
}; };
provider.putTestResource(Resource.REVOKE_CERT, resourceUrl); provider.putTestResource(Resource.REVOKE_CERT, resourceUrl);
@ -184,6 +206,12 @@ public class CertificateTest {
assertThat(certRequested, is(true)); assertThat(certRequested, is(true));
return originalCert; return originalCert;
} }
@Override
public Collection<URI> getLinks(String relation) {
assertThat(relation, is("alternate"));
return null;
}
}; };
provider.putTestResource(Resource.REVOKE_CERT, resourceUrl); provider.putTestResource(Resource.REVOKE_CERT, resourceUrl);

View File

@ -24,6 +24,9 @@ import java.io.OutputStreamWriter;
import java.io.Writer; import java.io.Writer;
import java.lang.reflect.Constructor; import java.lang.reflect.Constructor;
import java.lang.reflect.Modifier; import java.lang.reflect.Modifier;
import java.net.MalformedURLException;
import java.net.URI;
import java.net.URL;
import java.security.KeyPair; import java.security.KeyPair;
import java.security.Security; import java.security.Security;
import java.security.cert.CertificateEncodingException; import java.security.cert.CertificateEncodingException;
@ -286,6 +289,18 @@ public class AcmeUtilsTest {
assertThat(pemFile.toByteArray(), is(originalFile.toByteArray())); 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. * Matches the given time.
*/ */