diff --git a/acme4j-it/src/test/java/org/shredzone/acme4j/it/OrderIT.java b/acme4j-it/src/test/java/org/shredzone/acme4j/it/OrderIT.java index c39c951b..5103df8c 100644 --- a/acme4j-it/src/test/java/org/shredzone/acme4j/it/OrderIT.java +++ b/acme4j-it/src/test/java/org/shredzone/acme4j/it/OrderIT.java @@ -36,8 +36,6 @@ import org.shredzone.acme4j.challenge.Challenge; import org.shredzone.acme4j.challenge.Dns01Challenge; import org.shredzone.acme4j.challenge.Http01Challenge; import org.shredzone.acme4j.challenge.TlsSni02Challenge; -import org.shredzone.acme4j.exception.AcmeException; -import org.shredzone.acme4j.exception.AcmeLazyLoadingException; import org.shredzone.acme4j.util.CSRBuilder; import org.shredzone.acme4j.util.CertificateUtils; @@ -48,17 +46,14 @@ public class OrderIT extends PebbleITBase { private static final String TEST_DOMAIN = "example.com"; - private final String bammbammUrl = System.getProperty("bammbammUrl", "http://localhost:14001"); - private final String bammbammHostname = System.getProperty("bammbammHostname", "bammbamm"); - - private BammBammClient client = new BammBammClient(bammbammUrl); - /** * Test if a certificate can be ordered via tns-sni-02 challenge. */ @Test public void testTlsSniValidation() throws Exception { orderCertificate(TEST_DOMAIN, auth -> { + BammBammClient client = getBammBammClient(); + TlsSni02Challenge challenge = auth.findChallenge(TlsSni02Challenge.TYPE); assertThat(challenge, is(notNullValue())); @@ -67,7 +62,7 @@ public class OrderIT extends PebbleITBase { X509Certificate cert = CertificateUtils.createTlsSni02Certificate( challengeKey, challenge.getSubject(), challenge.getSanB()); - client.dnsAddARecord(TEST_DOMAIN, bammbammHostname); + client.dnsAddARecord(TEST_DOMAIN, getBammBammHostname()); client.tlsSniAddCertificate(challenge.getSubject(), challengeKey.getPrivate(), cert); cleanup(() -> client.dnsRemoveARecord(TEST_DOMAIN)); @@ -83,10 +78,12 @@ public class OrderIT extends PebbleITBase { @Test public void testHttpValidation() throws Exception { orderCertificate(TEST_DOMAIN, auth -> { + BammBammClient client = getBammBammClient(); + Http01Challenge challenge = auth.findChallenge(Http01Challenge.TYPE); assertThat(challenge, is(notNullValue())); - client.dnsAddARecord(TEST_DOMAIN, bammbammHostname); + client.dnsAddARecord(TEST_DOMAIN, getBammBammHostname()); client.httpAddToken(challenge.getToken(), challenge.getAuthorization()); cleanup(() -> client.dnsRemoveARecord(TEST_DOMAIN)); @@ -102,6 +99,8 @@ public class OrderIT extends PebbleITBase { @Test public void testDnsValidation() throws Exception { orderCertificate(TEST_DOMAIN, auth -> { + BammBammClient client = getBammBammClient(); + Dns01Challenge challenge = auth.findChallenge(Dns01Challenge.TYPE); assertThat(challenge, is(notNullValue())); @@ -186,34 +185,6 @@ public class OrderIT extends PebbleITBase { assertThat(cert.getSubjectX500Principal().getName(), containsString("CN=" + domain)); } - /** - * Safely updates the authorization, catching checked exceptions. - * - * @param auth - * {@link Authorization} to update - */ - private void updateAuth(Authorization auth) { - try { - auth.update(); - } catch (AcmeException ex) { - throw new AcmeLazyLoadingException(auth, ex); - } - } - - /** - * Safely updates the order, catching checked exceptions. - * - * @param order - * {@link Order} to update - */ - private void updateOrder(Order order) { - try { - order.update(); - } catch (AcmeException ex) { - throw new AcmeLazyLoadingException(order, ex); - } - } - @FunctionalInterface private static interface Validator { Challenge prepare(Authorization auth) throws Exception; diff --git a/acme4j-it/src/test/java/org/shredzone/acme4j/it/OrderWildcardIT.java b/acme4j-it/src/test/java/org/shredzone/acme4j/it/OrderWildcardIT.java new file mode 100644 index 00000000..10bce357 --- /dev/null +++ b/acme4j-it/src/test/java/org/shredzone/acme4j/it/OrderWildcardIT.java @@ -0,0 +1,132 @@ +/* + * acme4j - Java ACME client + * + * Copyright (C) 2017 Richard "Shred" Körber + * http://acme4j.shredzone.org + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + */ +package org.shredzone.acme4j.it; + +import static java.util.concurrent.TimeUnit.SECONDS; +import static java.util.stream.Collectors.toList; +import static org.awaitility.Awaitility.await; +import static org.hamcrest.Matchers.*; +import static org.junit.Assert.*; + +import java.security.KeyPair; +import java.security.cert.X509Certificate; +import java.time.Duration; +import java.time.Instant; +import java.time.temporal.ChronoUnit; +import java.util.List; + +import org.bouncycastle.asn1.x509.GeneralName; +import org.junit.Test; +import org.shredzone.acme4j.Account; +import org.shredzone.acme4j.AccountBuilder; +import org.shredzone.acme4j.Authorization; +import org.shredzone.acme4j.Certificate; +import org.shredzone.acme4j.Order; +import org.shredzone.acme4j.Session; +import org.shredzone.acme4j.Status; +import org.shredzone.acme4j.challenge.Dns01Challenge; +import org.shredzone.acme4j.util.CSRBuilder; + +/** + * Tests a complete wildcard certificate order. Wildcard certificates currently only + * support dns-01 challenge. + */ +public class OrderWildcardIT extends PebbleITBase { + + private static final String TEST_DOMAIN = "example.com"; + private static final String TEST_WILDCARD_DOMAIN = "*.example.com"; + + /** + * Test if a wildcard certificate can be ordered via dns-01 challenge. + */ + @Test + public void testDnsValidation() throws Exception { + BammBammClient client = getBammBammClient(); + KeyPair keyPair = createKeyPair(); + Session session = new Session(pebbleURI(), keyPair); + + Account account = new AccountBuilder() + .agreeToTermsOfService() + .create(session); + + KeyPair domainKeyPair = createKeyPair(); + + Instant notBefore = Instant.now().truncatedTo(ChronoUnit.MILLIS); + Instant notAfter = notBefore.plus(Duration.ofDays(20L)); + + Order order = account.newOrder() + .domain(TEST_WILDCARD_DOMAIN) + .domain(TEST_DOMAIN) + .notBefore(notBefore) + .notAfter(notAfter) + .create(); + assertThat(order.getNotBefore(), is(notBefore)); + assertThat(order.getNotAfter(), is(notAfter)); + assertThat(order.getStatus(), is(Status.PENDING)); + + for (Authorization auth : order.getAuthorizations()) { + assertThat(auth.getDomain(), is(TEST_DOMAIN)); + assertThat(auth.getStatus(), is(Status.PENDING)); + + Dns01Challenge challenge = auth.findChallenge(Dns01Challenge.TYPE); + assertThat(challenge, is(notNullValue())); + + String challengeDomainName = "_acme-challenge." + TEST_DOMAIN; + + client.dnsAddTxtRecord(challengeDomainName, challenge.getDigest()); + cleanup(() -> client.dnsRemoveTxtRecord(challengeDomainName)); + + challenge.trigger(); + + await() + .pollInterval(1, SECONDS) + .timeout(30, SECONDS) + .conditionEvaluationListener(cond -> updateAuth(auth)) + .until(auth::getStatus, not(isOneOf(Status.PENDING, Status.PROCESSING))); + + if (auth.getStatus() != Status.VALID) { + fail("Authorization failed"); + } + } + + CSRBuilder csr = new CSRBuilder(); + csr.addDomain(TEST_DOMAIN); + csr.addDomain(TEST_WILDCARD_DOMAIN); + csr.sign(domainKeyPair); + byte[] encodedCsr = csr.getEncoded(); + + order.execute(encodedCsr); + + await() + .pollInterval(1, SECONDS) + .timeout(30, SECONDS) + .conditionEvaluationListener(cond -> updateOrder(order)) + .until(order::getStatus, not(isOneOf(Status.PENDING, Status.PROCESSING))); + + + Certificate certificate = order.getCertificate(); + X509Certificate cert = certificate.getCertificate(); + assertThat(cert, not(nullValue())); + assertThat(cert.getNotAfter(), not(nullValue())); + assertThat(cert.getNotBefore(), not(nullValue())); + assertThat(cert.getSubjectX500Principal().getName(), containsString("CN=" + TEST_DOMAIN)); + + List san = cert.getSubjectAlternativeNames().stream() + .filter(it -> ((Number) it.get(0)).intValue() == GeneralName.dNSName) + .map(it -> (String) it.get(1)) + .collect(toList()); + assertThat(san, contains(TEST_DOMAIN, TEST_WILDCARD_DOMAIN)); + } + +} diff --git a/acme4j-it/src/test/java/org/shredzone/acme4j/it/PebbleITBase.java b/acme4j-it/src/test/java/org/shredzone/acme4j/it/PebbleITBase.java index fb615b76..fd344a0a 100644 --- a/acme4j-it/src/test/java/org/shredzone/acme4j/it/PebbleITBase.java +++ b/acme4j-it/src/test/java/org/shredzone/acme4j/it/PebbleITBase.java @@ -23,6 +23,10 @@ import java.util.ArrayList; import java.util.List; import org.junit.After; +import org.shredzone.acme4j.Authorization; +import org.shredzone.acme4j.Order; +import org.shredzone.acme4j.exception.AcmeException; +import org.shredzone.acme4j.exception.AcmeLazyLoadingException; import org.shredzone.acme4j.util.KeyPairUtils; /** @@ -37,6 +41,11 @@ public abstract class PebbleITBase { private final String pebbleHost = System.getProperty("pebbleHost", "localhost"); private final int pebblePort = Integer.parseInt(System.getProperty("pebblePort", "14000")); + private final String bammbammUrl = System.getProperty("bammbammUrl", "http://localhost:14001"); + private final String bammbammHostname = System.getProperty("bammbammHostname", "bammbamm"); + + private BammBammClient bammBammClient; + private final List cleanup = new ArrayList<>(); @After @@ -58,6 +67,23 @@ public abstract class PebbleITBase { return URI.create("acme://pebble/" + pebbleHost + ":" + pebblePort); } + /** + * @return {@link BammBammClient} singleton instance. + */ + protected BammBammClient getBammBammClient() { + if (bammBammClient == null) { + bammBammClient = new BammBammClient(bammbammUrl); + } + return bammBammClient; + } + + /** + * @return Hostname or IP address of the BammBamm server. + */ + protected String getBammBammHostname() { + return bammbammHostname; + } + /** * Creates a fresh key pair. * @@ -82,6 +108,34 @@ public abstract class PebbleITBase { assertThat(url.getPath(), not(isEmptyOrNullString())); } + /** + * Safely updates the authorization, catching checked exceptions. + * + * @param auth + * {@link Authorization} to update + */ + protected void updateAuth(Authorization auth) { + try { + auth.update(); + } catch (AcmeException ex) { + throw new AcmeLazyLoadingException(auth, ex); + } + } + + /** + * Safely updates the order, catching checked exceptions. + * + * @param order + * {@link Order} to update + */ + protected void updateOrder(Order order) { + try { + order.update(); + } catch (AcmeException ex) { + throw new AcmeLazyLoadingException(order, ex); + } + } + @FunctionalInterface public static interface CleanupCallback { void cleanup() throws Exception;