From a648a513f6ad71df7a2fd554c5857cc47cd6dffa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Richard=20K=C3=B6rber?= Date: Sat, 20 May 2023 17:20:13 +0200 Subject: [PATCH] Find certificates by issuer --- .../org/shredzone/acme4j/Certificate.java | 38 +++++++++++++++++ .../org/shredzone/acme4j/CertificateTest.java | 41 ++++++++++++++----- .../shredzone/acme4j/toolbox/TestUtils.java | 2 + 3 files changed, 70 insertions(+), 11 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 431041d8..827f1c8f 100644 --- a/acme4j-client/src/main/java/org/shredzone/acme4j/Certificate.java +++ b/acme4j-client/src/main/java/org/shredzone/acme4j/Certificate.java @@ -22,6 +22,7 @@ import java.io.Writer; import java.net.MalformedURLException; import java.net.URL; import java.security.KeyPair; +import java.security.Principal; import java.security.Security; import java.security.cert.CertificateEncodingException; import java.security.cert.X509Certificate; @@ -134,6 +135,43 @@ public class Certificate extends AcmeResource { return alternateCerts; } + /** + * Checks if this certificate was issued by the given issuer name. + * + * @param issuer + * Issuer name to check against, case-sensitive + * @return {@code true} if this issuer name was found in the certificate chain as + * issuer, {@code false} otherwise. + * @since 3.0.0 + */ + public boolean isIssuedBy(String issuer) { + var issuerCn = "CN=" + issuer; + return getCertificateChain().stream() + .map(X509Certificate::getIssuerX500Principal) + .map(Principal::getName) + .anyMatch(issuerCn::equals); + } + + /** + * Finds a {@link Certificate} that was issued by the given issuer name. + * + * @param issuer + * Issuer name to check against, case-sensitive + * @return Certificate that was issued by that issuer, or {@code empty} if there was + * none. The returned {@link Certificate} may be this instance, or one of the + * {@link #getAlternateCertificates()} instances. If multiple certificates are issued + * by that issuer, the first one that was found is returned. + * @since 3.0.0 + */ + public Optional findCertificate(String issuer) { + if (isIssuedBy(issuer)) { + return Optional.of(this); + } + return getAlternateCertificates().stream() + .filter(c -> c.isIssuedBy(issuer)) + .findFirst(); + } + /** * Writes the certificate to the given writer. It is written in PEM format, with the * end-entity cert coming first, followed by the intermediate certificates. 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 b22affd6..02ff0c80 100644 --- a/acme4j-client/src/test/java/org/shredzone/acme4j/CertificateTest.java +++ b/acme4j-client/src/test/java/org/shredzone/acme4j/CertificateTest.java @@ -15,8 +15,7 @@ package org.shredzone.acme4j; import static net.javacrumbs.jsonunit.assertj.JsonAssertions.assertThatJson; import static org.assertj.core.api.Assertions.assertThat; -import static org.shredzone.acme4j.toolbox.TestUtils.getJSON; -import static org.shredzone.acme4j.toolbox.TestUtils.url; +import static org.shredzone.acme4j.toolbox.TestUtils.*; import java.io.ByteArrayOutputStream; import java.io.IOException; @@ -49,6 +48,8 @@ public class CertificateTest { private final URL resourceUrl = url("http://example.com/acme/resource"); private final URL locationUrl = url("http://example.com/acme/certificate"); + private final URL alternate1Url = url("https://example.com/acme/alt-cert/1"); + private final URL alternate2Url = url("https://example.com/acme/alt-cert/2"); /** * Test that a certificate can be downloaded. @@ -56,26 +57,32 @@ public class CertificateTest { @Test public void testDownload() throws Exception { var originalCert = TestUtils.createCertificate("/cert.pem"); + var alternateCert = TestUtils.createCertificate("/certid-cert.pem"); var provider = new TestableConnectionProvider() { + List sendCert; + @Override public int sendCertificateRequest(URL url, Login login) { - assertThat(url).isEqualTo(locationUrl); + assertThat(url).isIn(locationUrl, alternate1Url, alternate2Url); assertThat(login).isNotNull(); + if (locationUrl.equals(url)) { + sendCert = originalCert; + } else { + sendCert = alternateCert; + } return HttpURLConnection.HTTP_OK; } @Override public List readCertificates() { - return originalCert; + return sendCert; } @Override public Collection getLinks(String relation) { assertThat(relation).isEqualTo("alternate"); - return Arrays.asList( - url("https://example.com/acme/alt-cert/1"), - url("https://example.com/acme/alt-cert/2")); + return Arrays.asList(alternate1Url, alternate2Url); } }; @@ -108,21 +115,33 @@ public class CertificateTest { } assertThat(writtenPem).isEqualTo(originalPem); + assertThat(cert.isIssuedBy("The ACME CA X1")).isFalse(); + assertThat(cert.isIssuedBy(CERT_ISSUER)).isTrue(); + assertThat(cert.getAlternates()).isNotNull(); assertThat(cert.getAlternates()).hasSize(2); - assertThat(cert.getAlternates()).element(0).isEqualTo(url("https://example.com/acme/alt-cert/1")); - assertThat(cert.getAlternates()).element(1).isEqualTo(url("https://example.com/acme/alt-cert/2")); + assertThat(cert.getAlternates()).element(0).isEqualTo(alternate1Url); + assertThat(cert.getAlternates()).element(1).isEqualTo(alternate2Url); assertThat(cert.getAlternateCertificates()).isNotNull(); assertThat(cert.getAlternateCertificates()).hasSize(2); assertThat(cert.getAlternateCertificates()) .element(0) .extracting(Certificate::getLocation) - .isEqualTo(url("https://example.com/acme/alt-cert/1")); + .isEqualTo(alternate1Url); assertThat(cert.getAlternateCertificates()) .element(1) .extracting(Certificate::getLocation) - .isEqualTo(url("https://example.com/acme/alt-cert/2")); + .isEqualTo(alternate2Url); + + assertThat(cert.findCertificate("The ACME CA X1")). + isEmpty(); + assertThat(cert.findCertificate(CERT_ISSUER).orElseThrow()) + .isSameAs(cert); + assertThat(cert.findCertificate("minica root ca 3a1356").orElseThrow()) + .isSameAs(cert.getAlternateCertificates().get(0)); + assertThat(cert.getAlternateCertificates().get(0).isIssuedBy("minica root ca 3a1356")) + .isTrue(); provider.close(); } diff --git a/acme4j-client/src/test/java/org/shredzone/acme4j/toolbox/TestUtils.java b/acme4j-client/src/test/java/org/shredzone/acme4j/toolbox/TestUtils.java index 4e612fdb..1570aec7 100644 --- a/acme4j-client/src/test/java/org/shredzone/acme4j/toolbox/TestUtils.java +++ b/acme4j-client/src/test/java/org/shredzone/acme4j/toolbox/TestUtils.java @@ -74,6 +74,8 @@ public final class TestUtils { public static final String DUMMY_NONCE = Base64.getUrlEncoder().withoutPadding().encodeToString("foo-nonce-foo".getBytes()); + public static final String CERT_ISSUER = "Pebble Intermediate CA 645fc5"; + public static final NetworkSettings DEFAULT_NETWORK_SETTINGS = new NetworkSettings(); private TestUtils() {