mirror of https://github.com/shred/acme4j
Add alternate certificates support
parent
7d83ef0e80
commit
4c34f9afb5
|
@ -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.
|
||||||
|
|
|
@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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.
|
||||||
*/
|
*/
|
||||||
|
|
Loading…
Reference in New Issue