001/* 002 * acme4j - Java ACME client 003 * 004 * Copyright (C) 2017 Richard "Shred" Körber 005 * http://acme4j.shredzone.org 006 * 007 * Licensed under the Apache License, Version 2.0 (the "License"); 008 * you may not use this file except in compliance with the License. 009 * 010 * This program is distributed in the hope that it will be useful, 011 * but WITHOUT ANY WARRANTY; without even the implied warranty of 012 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. 013 */ 014package org.shredzone.acme4j.it.pebble; 015 016import static java.util.concurrent.TimeUnit.SECONDS; 017import static org.assertj.core.api.Assertions.assertThat; 018import static org.awaitility.Awaitility.await; 019import static org.junit.jupiter.api.Assertions.assertThrows; 020 021import java.net.URI; 022import java.security.KeyPair; 023import java.security.cert.X509Certificate; 024import java.time.Duration; 025import java.time.Instant; 026import java.time.temporal.ChronoUnit; 027 028import org.junit.jupiter.api.Test; 029import org.shredzone.acme4j.AccountBuilder; 030import org.shredzone.acme4j.Authorization; 031import org.shredzone.acme4j.Certificate; 032import org.shredzone.acme4j.Login; 033import org.shredzone.acme4j.RevocationReason; 034import org.shredzone.acme4j.Session; 035import org.shredzone.acme4j.Status; 036import org.shredzone.acme4j.challenge.Challenge; 037import org.shredzone.acme4j.challenge.Dns01Challenge; 038import org.shredzone.acme4j.challenge.Http01Challenge; 039import org.shredzone.acme4j.challenge.TlsAlpn01Challenge; 040import org.shredzone.acme4j.exception.AcmeException; 041import org.shredzone.acme4j.exception.AcmeServerException; 042 043/** 044 * Tests a complete certificate order with different challenges. 045 */ 046public class OrderIT extends PebbleITBase { 047 048 private static final String TEST_DOMAIN = "example.com"; 049 050 /** 051 * Test if a certificate can be ordered via http-01 challenge. 052 */ 053 @Test 054 public void testHttpValidation() throws Exception { 055 orderCertificate(TEST_DOMAIN, auth -> { 056 var client = getBammBammClient(); 057 058 var challenge = auth.findChallenge(Http01Challenge.class).orElseThrow(); 059 060 client.httpAddToken(challenge.getToken(), challenge.getAuthorization()); 061 062 cleanup(() -> client.httpRemoveToken(challenge.getToken())); 063 064 return challenge; 065 }, OrderIT::standardRevoker); 066 } 067 068 /** 069 * Test if a certificate can be ordered via dns-01 challenge. 070 */ 071 @Test 072 public void testDnsValidation() throws Exception { 073 orderCertificate(TEST_DOMAIN, auth -> { 074 var client = getBammBammClient(); 075 076 var challenge = auth.findChallenge(Dns01Challenge.class).orElseThrow(); 077 078 var challengeDomainName = Dns01Challenge.toRRName(auth.getIdentifier()); 079 080 client.dnsAddTxtRecord(challengeDomainName, challenge.getDigest()); 081 082 cleanup(() -> client.dnsRemoveTxtRecord(challengeDomainName)); 083 084 return challenge; 085 }, OrderIT::standardRevoker); 086 } 087 088 /** 089 * Test if a certificate can be ordered via tns-alpn-01 challenge. 090 */ 091 @Test 092 public void testTlsAlpnValidation() throws Exception { 093 orderCertificate(TEST_DOMAIN, auth -> { 094 var client = getBammBammClient(); 095 096 var challenge = auth.findChallenge(TlsAlpn01Challenge.class).orElseThrow(); 097 098 client.tlsAlpnAddCertificate( 099 auth.getIdentifier().getDomain(), 100 challenge.getAuthorization()); 101 102 cleanup(() -> client.tlsAlpnRemoveCertificate(auth.getIdentifier().getDomain())); 103 104 return challenge; 105 }, OrderIT::standardRevoker); 106 } 107 108 /** 109 * Test if a certificate can be revoked by its domain key. 110 */ 111 @Test 112 public void testDomainKeyRevocation() throws Exception { 113 orderCertificate(TEST_DOMAIN, auth -> { 114 var client = getBammBammClient(); 115 116 var challenge = auth.findChallenge(Http01Challenge.class).orElseThrow(); 117 118 client.httpAddToken(challenge.getToken(), challenge.getAuthorization()); 119 120 cleanup(() -> client.httpRemoveToken(challenge.getToken())); 121 122 return challenge; 123 }, OrderIT::domainKeyRevoker); 124 } 125 126 /** 127 * Runs the complete process of ordering a certificate. 128 * 129 * @param domain 130 * Name of the domain to order a certificate for 131 * @param validator 132 * {@link Validator} that finds and prepares a {@link Challenge} for domain 133 * validation 134 * @param revoker 135 * {@link Revoker} that finally revokes the certificate 136 */ 137 private void orderCertificate(String domain, Validator validator, Revoker revoker) 138 throws Exception { 139 var keyPair = createKeyPair(); 140 var session = new Session(pebbleURI()); 141 142 var account = new AccountBuilder() 143 .agreeToTermsOfService() 144 .useKeyPair(keyPair) 145 .create(session); 146 147 var domainKeyPair = createKeyPair(); 148 149 var notBefore = Instant.now().truncatedTo(ChronoUnit.SECONDS); 150 var notAfter = notBefore.plus(Duration.ofDays(20L)); 151 152 var order = account.newOrder() 153 .domain(domain) 154 .notBefore(notBefore) 155 .notAfter(notAfter) 156 .create(); 157 assertThat(order.getNotBefore().orElseThrow()).isEqualTo(notBefore); 158 assertThat(order.getNotAfter().orElseThrow()).isEqualTo(notAfter); 159 assertThat(order.getStatus()).isEqualTo(Status.PENDING); 160 161 for (var auth : order.getAuthorizations()) { 162 assertThat(auth.getIdentifier().getDomain()).isEqualTo(domain); 163 assertThat(auth.getStatus()).isEqualTo(Status.PENDING); 164 165 if (auth.getStatus() == Status.VALID) { 166 continue; 167 } 168 169 var challenge = validator.prepare(auth); 170 challenge.trigger(); 171 172 await() 173 .pollInterval(1, SECONDS) 174 .timeout(30, SECONDS) 175 .conditionEvaluationListener(cond -> updateAuth(auth)) 176 .untilAsserted(() -> assertThat(auth.getStatus()).isNotIn(Status.PENDING, Status.PROCESSING)); 177 178 assertThat(auth.getStatus()).isEqualTo(Status.VALID); 179 } 180 181 order.execute(domainKeyPair); 182 183 await() 184 .pollInterval(1, SECONDS) 185 .timeout(30, SECONDS) 186 .conditionEvaluationListener(cond -> updateOrder(order)) 187 .untilAsserted(() -> assertThat(order.getStatus()) 188 .isNotIn(Status.PENDING, Status.PROCESSING, Status.READY)); 189 190 assertThat(order.getStatus()).isEqualTo(Status.VALID); 191 192 var certificate = order.getCertificate(); 193 var cert = certificate.getCertificate(); 194 assertThat(cert).isNotNull(); 195 assertThat(cert.getNotBefore().toInstant()).isEqualTo(notBefore); 196 assertThat(cert.getNotAfter().toInstant()).isEqualTo(notAfter); 197 assertThat(cert.getSubjectX500Principal().getName()).contains("CN=" + domain); 198 199 for (var auth : order.getAuthorizations()) { 200 assertThat(auth.getStatus()).isEqualTo(Status.VALID); 201 auth.deactivate(); 202 assertThat(auth.getStatus()).isEqualTo(Status.DEACTIVATED); 203 } 204 205 revoker.revoke(session, certificate, keyPair, domainKeyPair); 206 207 // Make sure certificate is revoked 208 var ex = assertThrows(AcmeException.class, () -> { 209 Login login2 = session.login(account.getLocation(), keyPair); 210 Certificate cert2 = login2.bindCertificate(certificate.getLocation()); 211 cert2.download(); 212 }, "Could download revoked cert"); 213 assertThat(ex.getMessage()).isEqualTo("HTTP 404"); 214 215 // Try to revoke again 216 var ex2 = assertThrows(AcmeServerException.class, 217 certificate::revoke, 218 "Could revoke again"); 219 assertThat(ex2.getProblem().getType()).isEqualTo(URI.create("urn:ietf:params:acme:error:alreadyRevoked")); 220 } 221 222 /** 223 * Revokes a certificate by calling {@link Certificate#revoke(RevocationReason)}. 224 * This is the standard way to revoke a certificate. 225 */ 226 private static void standardRevoker(Session session, Certificate certificate, 227 KeyPair keyPair, KeyPair domainKeyPair) throws Exception { 228 certificate.revoke(RevocationReason.KEY_COMPROMISE); 229 } 230 231 /** 232 * Revokes a certificate by calling 233 * {@link Certificate#revoke(Session, KeyPair, X509Certificate, RevocationReason)}. 234 * This way can be used when the account key was lost. 235 */ 236 private static void domainKeyRevoker(Session session, Certificate certificate, 237 KeyPair keyPair, KeyPair domainKeyPair) throws Exception { 238 Certificate.revoke(session, domainKeyPair, certificate.getCertificate(), 239 RevocationReason.KEY_COMPROMISE); 240 } 241 242 @FunctionalInterface 243 private interface Validator { 244 Challenge prepare(Authorization auth) throws Exception; 245 } 246 247 @FunctionalInterface 248 private interface Revoker { 249 void revoke(Session session, Certificate certificate, KeyPair keyPair, 250 KeyPair domainKeyPair) throws Exception; 251 } 252 253}