Changed the way the acme client works

This commit is contained in:
Ziver Koc 2021-08-20 23:09:24 +02:00
parent 708577debf
commit 98f53adcfb
2 changed files with 97 additions and 43 deletions

View file

@ -6,17 +6,14 @@ import java.net.URL;
import java.security.KeyPair;
import java.security.Security;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
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.*;
import org.shredzone.acme4j.challenge.Challenge;
import org.shredzone.acme4j.exception.AcmeException;
import org.shredzone.acme4j.util.CSRBuilder;
@ -44,69 +41,107 @@ public class AcmeClient {
private AcmeDataStore dataStore;
private AcmeChallengeFactory challengeFactory;
private Account acmeAccount;
private ArrayList<String> domains = new ArrayList<>();
private Order order;
private ArrayList<Challenge> challenges = new ArrayList<>();
public AcmeClient(AcmeDataStore dataStore, HttpServer httpServer) {
public AcmeClient(AcmeDataStore dataStore, HttpServer httpServer) throws AcmeException {
this(dataStore, new AcmeHttpChallengeFactory(httpServer), ACME_SERVER_LETSENCRYPT_PRODUCTION);
}
public AcmeClient(AcmeDataStore dataStore, AcmeChallengeFactory challengeFactory) {
public AcmeClient(AcmeDataStore dataStore, AcmeChallengeFactory challengeFactory) throws AcmeException {
this(dataStore, challengeFactory, ACME_SERVER_LETSENCRYPT_PRODUCTION);
}
/**
* Create a new instance of the ACME Client
* Create a new instance of the ACME Client and authenticates the user account towards
* the AMCE service, if no account exists then a new one will be created.
*/
public AcmeClient(AcmeDataStore dataStore, AcmeChallengeFactory challengeFactory, String acmeServerUrl) {
public AcmeClient(AcmeDataStore dataStore, AcmeChallengeFactory challengeFactory, String acmeServerUrl) throws AcmeException {
Security.addProvider(new BouncyCastleProvider());
this.dataStore = dataStore;
this.challengeFactory = challengeFactory;
this.acmeServerUrl = acmeServerUrl;
}
/**
* Generates a certificate for the given domains. Also takes care for the registration
* process.
*
* @param domains the domains to get a certificates for
* @return a certificate for the given domains.
*/
public X509Certificate fetchCertificate(String... domains) throws IOException, AcmeException {
// ------------------------------------------------
// Read in keys
// ------------------------------------------------
KeyPair userKeyPair = dataStore.loadUserKeyPair(); // Load the user key file. If there is no key file, create a new one.
KeyPair domainKeyPair = dataStore.loadDomainKeyPair(); // Load or create a key pair for the domains. This should not be the userKeyPair!
KeyPair domainKeyPair = dataStore.loadDomainKeyPair(); // Load or create a key pair for the domains. This should not be same as the userKeyPair!
if (userKeyPair == null) {
logger.fine("Creating new user keys.");
userKeyPair = KeyPairUtils.createKeyPair(KEY_SIZE);
dataStore.storeUserKeyPair(userKeyPair);
}
if (domainKeyPair == null) {
logger.fine("Creating new domain keys.");
domainKeyPair = KeyPairUtils.createKeyPair(KEY_SIZE);
dataStore.storeDomainKeyPair(domainKeyPair);
}
// ------------------------------------------------
// Start authorization process
// Start user authorization process
// ------------------------------------------------
Session session = new Session(acmeServerUrl);
Account acct = getAccount(session, userKeyPair); // Get the Account. If there is no account yet, create a new one.
Order order = acct.newOrder().domains(domains).create(); // Order the certificate
acmeAccount = getAccount(session, userKeyPair); // Get the Account. If there is no account yet, create a new one.
}
/**
* Add a domain that the certificate should be valid for. This method can only be called before
* the {@link #prepareRequest()} method has been called.
*
* @param domains the domains to add to the certificate
*/
public void addDomain(String... domains) {
Collections.addAll(this.domains, domains);
}
/**
* This method will prepare the request for the ACME service. Any manual action must be taken after this
* method has been called and before the {@link #requestCertificate()} method is called.
*
* @throws AcmeException
*/
public void prepareRequest() throws AcmeException {
order = acmeAccount.newOrder().domains(domains).create(); // Order the certificate
// Perform all required authorizations
for (Authorization auth : order.getAuthorizations()) {
if (auth.getStatus() == Status.VALID)
continue; // The authorization is already valid. No need to process a challenge.
execDomainChallenge(auth);
challenges.add(challengeFactory.createChallenge(auth));
}
}
/**
* Connects to the ACME service and requests a certificate to be generated.
* <p>
* Note that before this method is called the {@link #prepareRequest()} method must be called to prepare
* the challenge requests. The reason for this is as some challenges require manual intervention between
* the preparation and the actual request.
*
* @return a certificate for the given domains.
*/
public X509Certificate requestCertificate() throws IOException, AcmeException {
if (order == null)
throw new IllegalStateException("prepareRequest() method has not been called before the request of certificate.");
// Perform all required domain authorizations
for (Challenge challenge : challenges) {
execDomainChallenge(challenge);
}
// Generate a "Certificate Signing Request" for all of the domains, and sign it with the domain key pair.
// Generate one "Certificate Signing Request" for all the domains, and sign it with the domain key pair.
CSRBuilder csrBuilder = new CSRBuilder();
csrBuilder.addDomains(domains);
csrBuilder.sign(domainKeyPair);
csrBuilder.sign(dataStore.loadDomainKeyPair());
order.execute(csrBuilder.getEncoded()); // Order the certificate
@ -137,6 +172,10 @@ public class AcmeClient {
logger.info("The certificate for domains '" + StringUtil.join(",", domains) + "' has been successfully generated.");
// Cleanup
order = null;
challenges.clear();
return certificate.getCertificate();
}
@ -170,19 +209,17 @@ public class AcmeClient {
return account;
}
/**
* Authorize a domain. It will be associated with your account, so you will be able to
* retrieve a signed certificate for the domain later.
*
* @param authorization {@link Authorization} to perform
* @param challenge {@link Challenge} to be performed
*/
private void execDomainChallenge(Authorization authorization) throws AcmeException {
logger.info("Authorization for domain: " + authorization.getIdentifier().getDomain());
Challenge challenge = null;
private void execDomainChallenge(Challenge challenge) throws AcmeException {
logger.info("Executing challenge: " + challenge);
try {
challenge = challengeFactory.createChallenge(authorization);
// If the challenge is already verified, there's no need to execute it again.
if (challenge.getStatus() == Status.VALID)
return;
@ -197,9 +234,7 @@ public class AcmeClient {
if (challenge.getStatus() == Status.VALID) {
break;
} else if (challenge.getStatus() == Status.INVALID) {
//throw new AcmeException("Certificate challenge failed: " + challenge.getError());
logger.severe("Certificate challenge failed: " + challenge.getError());
challenge.trigger();
throw new AcmeException("Certificate challenge failed: " + challenge.getError());
}
// Wait for a few seconds
@ -213,7 +248,7 @@ public class AcmeClient {
// All reattempts are used up and there is still no valid authorization?
if (challenge.getStatus() != Status.VALID)
throw new AcmeException("Failed to pass the challenge for domain " + authorization.getIdentifier().getDomain());
throw new AcmeException("Failed to pass the challenge: " + challenge.getError());
} catch (InterruptedException ex) {
logger.log(Level.SEVERE, "Interrupted", ex);
} finally {

View file

@ -19,9 +19,6 @@ public class AcmeManualDnsChallengeFactory implements AcmeChallengeFactory {
@Override
public Challenge createChallenge(Authorization authorization) throws AcmeException {
if (challengeCache.containsKey(authorization.getIdentifier().getDomain()))
return challengeCache.get(authorization.getIdentifier().getDomain());
Dns01Challenge challenge = authorization.findChallenge(Dns01Challenge.class);
if (challenge == null) {
throw new AcmeException("Found no " + Dns01Challenge.TYPE + " challenge.");
@ -31,11 +28,33 @@ public class AcmeManualDnsChallengeFactory implements AcmeChallengeFactory {
logger.warning(
"---------------------------- ATTENTION ----------------------------\n" +
"For certificate challenge to pass please create a DNS TXT record:\n" +
"_acme-challenge." + authorization.getIdentifier().getDomain() + ". IN TXT " + challenge.getDigest() + "\n" +
"Please deploy a DNS TXT record under the name\n" +
getChallengeDnsName(authorization.getIdentifier().getDomain()) + " with the following value:\n\n" +
challenge.getDigest() + "\n\n" +
"Continue the process once this is deployed." +
"--------------------------------------------------------------------");
challengeCache.put(authorization.getIdentifier().getDomain(), challenge);
return challenge;
}
/**
* Returns the name of the record where the challenge value has to be assigned to.
*
* @param domain the specific domain
* @return a String DNS record name
*/
public String getChallengeDnsName(String domain) {
return "_acme-challenge." + domain;
}
/**
* Gives the value of the required DNS record.
*
* @param domain the specific domain
* @return a special String value that needs to be added to the domains DNS record
*/
public String getChallengeDnsValue(String domain) {
return challengeCache.get(domain).getDigest();
}
}