mirror of https://github.com/shred/acme4j
Clean up example and make it more comprehensible
parent
fda8c1e409
commit
e19c11d407
|
@ -33,7 +33,6 @@ import org.shredzone.acme4j.challenge.Dns01Challenge;
|
||||||
import org.shredzone.acme4j.challenge.Http01Challenge;
|
import org.shredzone.acme4j.challenge.Http01Challenge;
|
||||||
import org.shredzone.acme4j.exception.AcmeConflictException;
|
import org.shredzone.acme4j.exception.AcmeConflictException;
|
||||||
import org.shredzone.acme4j.exception.AcmeException;
|
import org.shredzone.acme4j.exception.AcmeException;
|
||||||
import org.shredzone.acme4j.exception.AcmeUnauthorizedException;
|
|
||||||
import org.shredzone.acme4j.util.CSRBuilder;
|
import org.shredzone.acme4j.util.CSRBuilder;
|
||||||
import org.shredzone.acme4j.util.CertificateUtils;
|
import org.shredzone.acme4j.util.CertificateUtils;
|
||||||
import org.shredzone.acme4j.util.KeyPairUtils;
|
import org.shredzone.acme4j.util.KeyPairUtils;
|
||||||
|
@ -46,17 +45,28 @@ import org.slf4j.LoggerFactory;
|
||||||
* Pass the names of the domains as parameters.
|
* Pass the names of the domains as parameters.
|
||||||
*/
|
*/
|
||||||
public class ClientTest {
|
public class ClientTest {
|
||||||
|
// File name of the User Key Pair
|
||||||
private static final File USER_KEY_FILE = new File("user.key");
|
private static final File USER_KEY_FILE = new File("user.key");
|
||||||
|
|
||||||
|
// File name of the Domain Key Pair
|
||||||
private static final File DOMAIN_KEY_FILE = new File("domain.key");
|
private static final File DOMAIN_KEY_FILE = new File("domain.key");
|
||||||
private static final File DOMAIN_CERT_FILE = new File("domain.crt");
|
|
||||||
private static final File CERT_CHAIN_FILE = new File("chain.crt");
|
// File name of the CSR
|
||||||
private static final File DOMAIN_CHAIN_FILE = new File("domain-chain.crt");
|
|
||||||
private static final File DOMAIN_CSR_FILE = new File("domain.csr");
|
private static final File DOMAIN_CSR_FILE = new File("domain.csr");
|
||||||
|
|
||||||
|
// File name of the signed certificate
|
||||||
|
private static final File DOMAIN_CHAIN_FILE = new File("domain-chain.crt");
|
||||||
|
|
||||||
|
//Challenge type to be used
|
||||||
|
private static final ChallengeType CHALLENGE_TYPE = ChallengeType.HTTP;
|
||||||
|
|
||||||
|
// RSA key size of generated key pairs
|
||||||
private static final int KEY_SIZE = 2048;
|
private static final int KEY_SIZE = 2048;
|
||||||
|
|
||||||
private static final Logger LOG = LoggerFactory.getLogger(ClientTest.class);
|
private static final Logger LOG = LoggerFactory.getLogger(ClientTest.class);
|
||||||
|
|
||||||
|
private enum ChallengeType { HTTP, DNS, TLSSNI }
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generates a certificate for the given domains. Also takes care for the registration
|
* Generates a certificate for the given domains. Also takes care for the registration
|
||||||
* process.
|
* process.
|
||||||
|
@ -65,85 +75,196 @@ public class ClientTest {
|
||||||
* Domains to get a common certificate for
|
* Domains to get a common certificate for
|
||||||
*/
|
*/
|
||||||
public void fetchCertificate(Collection<String> domains) throws IOException, AcmeException {
|
public void fetchCertificate(Collection<String> domains) throws IOException, AcmeException {
|
||||||
// Load or create a key pair for the user's account
|
// Load the user key file. If there is no key file, create a new one.
|
||||||
boolean createdNewKeyPair = false;
|
KeyPair userKeyPair = loadOrCreateUserKeyPair();
|
||||||
|
|
||||||
KeyPair userKeyPair;
|
// Create a session for Let's Encrypt.
|
||||||
if (USER_KEY_FILE.exists()) {
|
|
||||||
try (FileReader fr = new FileReader(USER_KEY_FILE)) {
|
|
||||||
userKeyPair = KeyPairUtils.readKeyPair(fr);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
userKeyPair = KeyPairUtils.createKeyPair(KEY_SIZE);
|
|
||||||
try (FileWriter fw = new FileWriter(USER_KEY_FILE)) {
|
|
||||||
KeyPairUtils.writeKeyPair(userKeyPair, fw);
|
|
||||||
}
|
|
||||||
createdNewKeyPair = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create a session for Let's Encrypt
|
|
||||||
// Use "acme://letsencrypt.org" for production server
|
// Use "acme://letsencrypt.org" for production server
|
||||||
Session session = new Session("acme://letsencrypt.org/staging", userKeyPair);
|
Session session = new Session("acme://letsencrypt.org/staging", userKeyPair);
|
||||||
|
|
||||||
// Register a new user
|
// Get the Registration to the account.
|
||||||
Registration reg = null;
|
// If there is no account yet, create a new one.
|
||||||
|
Registration reg = findOrRegisterAccount(session);
|
||||||
|
|
||||||
|
// Separately authorize every requested domain.
|
||||||
|
for (String domain : domains) {
|
||||||
|
authorize(reg, domain);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load or create a key pair for the domains. This should not be the userKeyPair!
|
||||||
|
KeyPair domainKeyPair = loadOrCreateDomainKeyPair();
|
||||||
|
|
||||||
|
// Generate a CSR for all of the domains, and sign it with the domain key pair.
|
||||||
|
CSRBuilder csrb = new CSRBuilder();
|
||||||
|
csrb.addDomains(domains);
|
||||||
|
csrb.sign(domainKeyPair);
|
||||||
|
|
||||||
|
// Write the CSR to a file, for later use.
|
||||||
|
try (Writer out = new FileWriter(DOMAIN_CSR_FILE)) {
|
||||||
|
csrb.write(out);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Now request a signed certificate.
|
||||||
|
Certificate certificate = reg.requestCertificate(csrb.getEncoded());
|
||||||
|
|
||||||
|
LOG.info("Success! The certificate for domains " + domains + " has been generated!");
|
||||||
|
LOG.info("Certificate URI: " + certificate.getLocation());
|
||||||
|
|
||||||
|
// Download the leaf certificate and certificate chain.
|
||||||
|
X509Certificate cert = certificate.download();
|
||||||
|
X509Certificate[] chain = certificate.downloadChain();
|
||||||
|
|
||||||
|
// Write a combined file containing the certificate and chain.
|
||||||
|
try (FileWriter fw = new FileWriter(DOMAIN_CHAIN_FILE)) {
|
||||||
|
CertificateUtils.writeX509CertificateChain(fw, cert, chain);
|
||||||
|
}
|
||||||
|
|
||||||
|
// That's all! Configure your web server to use the DOMAIN_KEY_FILE and
|
||||||
|
// DOMAIN_CHAIN_FILE for the requested domans.
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Loads a user key pair from {@value #USER_KEY_FILE}. If the file does not exist,
|
||||||
|
* a new key pair is generated and saved.
|
||||||
|
* <p>
|
||||||
|
* Keep this key pair in a safe place! In a production environment, you will not be
|
||||||
|
* able to access your account again if you should lose the key pair.
|
||||||
|
*
|
||||||
|
* @return User's {@link KeyPair}.
|
||||||
|
*/
|
||||||
|
private KeyPair loadOrCreateUserKeyPair() throws IOException {
|
||||||
|
if (USER_KEY_FILE.exists()) {
|
||||||
|
// If there is a key file, read it
|
||||||
|
try (FileReader fr = new FileReader(USER_KEY_FILE)) {
|
||||||
|
return KeyPairUtils.readKeyPair(fr);
|
||||||
|
}
|
||||||
|
|
||||||
|
} else {
|
||||||
|
// If there is none, create a new key pair and save it
|
||||||
|
KeyPair userKeyPair = KeyPairUtils.createKeyPair(KEY_SIZE);
|
||||||
|
try (FileWriter fw = new FileWriter(USER_KEY_FILE)) {
|
||||||
|
KeyPairUtils.writeKeyPair(userKeyPair, fw);
|
||||||
|
}
|
||||||
|
return userKeyPair;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Loads a domain key pair from {@value #DOMAIN_KEY_FILE}. If the file does not exist,
|
||||||
|
* a new key pair is generated and saved.
|
||||||
|
*
|
||||||
|
* @return Domain {@link KeyPair}.
|
||||||
|
*/
|
||||||
|
private KeyPair loadOrCreateDomainKeyPair() throws IOException {
|
||||||
|
if (DOMAIN_KEY_FILE.exists()) {
|
||||||
|
try (FileReader fr = new FileReader(DOMAIN_KEY_FILE)) {
|
||||||
|
return KeyPairUtils.readKeyPair(fr);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
KeyPair domainKeyPair = KeyPairUtils.createKeyPair(KEY_SIZE);
|
||||||
|
try (FileWriter fw = new FileWriter(DOMAIN_KEY_FILE)) {
|
||||||
|
KeyPairUtils.writeKeyPair(domainKeyPair, fw);
|
||||||
|
}
|
||||||
|
return domainKeyPair;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Finds your {@link Registration} at the ACME server. It will be found by your user's
|
||||||
|
* public key. If your key is not known to the server yet, a new registration will be
|
||||||
|
* created.
|
||||||
|
* <p>
|
||||||
|
* This is a simple way of finding your {@link Registration}. A better way is to get
|
||||||
|
* the URI of your new registration with {@link Registration#getLocation()} and store
|
||||||
|
* it somewhere. If you need to get access to your account later, reconnect to it via
|
||||||
|
* {@link Registration#bind(Session, URI)} by using the stored location.
|
||||||
|
*
|
||||||
|
* @param session
|
||||||
|
* {@link Session} to bind with
|
||||||
|
* @return {@link Registration} connected to your account
|
||||||
|
*/
|
||||||
|
private Registration findOrRegisterAccount(Session session) throws AcmeException {
|
||||||
|
Registration reg;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
// Try to create a new Registration.
|
||||||
reg = new RegistrationBuilder().create(session);
|
reg = new RegistrationBuilder().create(session);
|
||||||
LOG.info("Registered a new user, URI: " + reg.getLocation());
|
LOG.info("Registered a new user, URI: " + reg.getLocation());
|
||||||
} catch (AcmeConflictException ex) {
|
|
||||||
LOG.trace("acme4j exception caught", ex);
|
|
||||||
reg = Registration.bind(session, ex.getLocation());
|
|
||||||
LOG.info("Account does already exist, URI: " + reg.getLocation());
|
|
||||||
}
|
|
||||||
|
|
||||||
|
// This is a new account. Let the user accept the Terms of Service.
|
||||||
|
// We won't be able to authorize domains until the ToS is accepted.
|
||||||
URI agreement = reg.getAgreement();
|
URI agreement = reg.getAgreement();
|
||||||
LOG.info("Terms of Service: " + agreement);
|
LOG.info("Terms of Service: " + agreement);
|
||||||
|
acceptAgreement(reg, agreement);
|
||||||
|
|
||||||
if (createdNewKeyPair) {
|
} catch (AcmeConflictException ex) {
|
||||||
boolean accepted = acceptAgreement(reg, agreement);
|
// The Key Pair is already registered. getLocation() contains the
|
||||||
if (!accepted) {
|
// URL of the existing registration's location. Bind it to the session.
|
||||||
return;
|
reg = Registration.bind(session, ex.getLocation());
|
||||||
}
|
LOG.info("Account does already exist, URI: " + reg.getLocation(), ex);
|
||||||
}
|
}
|
||||||
|
|
||||||
for (String domain : domains) {
|
return reg;
|
||||||
// Create a new authorization
|
|
||||||
Authorization auth = null;
|
|
||||||
try {
|
|
||||||
auth = reg.authorizeDomain(domain);
|
|
||||||
} catch (AcmeUnauthorizedException ex) {
|
|
||||||
// Maybe there are new T&C to accept?
|
|
||||||
LOG.trace("acme4j exception caught", ex);
|
|
||||||
boolean accepted = acceptAgreement(reg, agreement);
|
|
||||||
if (!accepted) {
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
// Then try again...
|
|
||||||
auth = reg.authorizeDomain(domain);
|
|
||||||
}
|
|
||||||
LOG.info("New authorization for domain " + domain);
|
|
||||||
|
|
||||||
// Uncomment a challenge...
|
/**
|
||||||
Challenge challenge = httpChallenge(auth, domain);
|
* Authorize a domain. It will be associated with your account, so you will be able to
|
||||||
// Challenge challenge = dnsChallenge(auth, domain);
|
* retrieve a signed certificate for the domain later.
|
||||||
// Challenge challenge = tlsSniChallenge(auth, domain);
|
* <p>
|
||||||
|
* You need separate authorizations for subdomains (e.g. "www" subdomain). Wildcard
|
||||||
|
* certificates are not currently supported.
|
||||||
|
*
|
||||||
|
* @param reg
|
||||||
|
* {@link Registration} of your account
|
||||||
|
* @param domain
|
||||||
|
* Name of the domain to authorize
|
||||||
|
*/
|
||||||
|
private void authorize(Registration reg, String domain) throws AcmeException {
|
||||||
|
// Authorize the domain.
|
||||||
|
Authorization auth = reg.authorizeDomain(domain);
|
||||||
|
LOG.info("Authorization for domain " + domain);
|
||||||
|
|
||||||
|
// Find the desired challenge and prepare it.
|
||||||
|
Challenge challenge = null;
|
||||||
|
switch (CHALLENGE_TYPE) {
|
||||||
|
case HTTP:
|
||||||
|
challenge = httpChallenge(auth, domain);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case DNS:
|
||||||
|
challenge = dnsChallenge(auth, domain);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case TLSSNI:
|
||||||
|
challenge = tlsSniChallenge(auth, domain);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
if (challenge == null) {
|
if (challenge == null) {
|
||||||
|
throw new AcmeException("No challenge found");
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the challenge is already verified, there's no need to execute it again.
|
||||||
|
if (challenge.getStatus() == Status.VALID) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Trigger the challenge
|
// Now trigger the challenge.
|
||||||
challenge.trigger();
|
challenge.trigger();
|
||||||
|
|
||||||
// Poll for the challenge to complete
|
// Poll for the challenge to complete.
|
||||||
try {
|
try {
|
||||||
int attempts = 10;
|
int attempts = 10;
|
||||||
while (challenge.getStatus() != Status.VALID && attempts-- > 0) {
|
while (challenge.getStatus() != Status.VALID && attempts-- > 0) {
|
||||||
|
// Did the authorization fail?
|
||||||
if (challenge.getStatus() == Status.INVALID) {
|
if (challenge.getStatus() == Status.INVALID) {
|
||||||
LOG.error("Challenge failed... Giving up.");
|
throw new AcmeException("Challenge failed... Giving up.");
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Wait for a few seconds
|
||||||
Thread.sleep(3000L);
|
Thread.sleep(3000L);
|
||||||
|
|
||||||
|
// Then update the status
|
||||||
challenge.update();
|
challenge.update();
|
||||||
}
|
}
|
||||||
} catch (InterruptedException ex) {
|
} catch (InterruptedException ex) {
|
||||||
|
@ -151,74 +272,33 @@ public class ClientTest {
|
||||||
Thread.currentThread().interrupt();
|
Thread.currentThread().interrupt();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// All reattempts are used up and there is still no valid authorization?
|
||||||
if (challenge.getStatus() != Status.VALID) {
|
if (challenge.getStatus() != Status.VALID) {
|
||||||
LOG.error("Failed to pass the challenge... Giving up.");
|
throw new AcmeException("Failed to pass the challenge for domain " + domain + ", ... Giving up.");
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load or create a key pair for the domain
|
|
||||||
KeyPair domainKeyPair;
|
|
||||||
if (DOMAIN_KEY_FILE.exists()) {
|
|
||||||
try (FileReader fr = new FileReader(DOMAIN_KEY_FILE)) {
|
|
||||||
domainKeyPair = KeyPairUtils.readKeyPair(fr);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
domainKeyPair = KeyPairUtils.createKeyPair(KEY_SIZE);
|
|
||||||
try (FileWriter fw = new FileWriter(DOMAIN_KEY_FILE)) {
|
|
||||||
KeyPairUtils.writeKeyPair(domainKeyPair, fw);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Generate a CSR for the domain
|
|
||||||
CSRBuilder csrb = new CSRBuilder();
|
|
||||||
csrb.addDomains(domains);
|
|
||||||
csrb.sign(domainKeyPair);
|
|
||||||
|
|
||||||
try (Writer out = new FileWriter(DOMAIN_CSR_FILE)) {
|
|
||||||
csrb.write(out);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Request a signed certificate
|
|
||||||
Certificate certificate = reg.requestCertificate(csrb.getEncoded());
|
|
||||||
LOG.info("Success! The certificate for domains " + domains + " has been generated!");
|
|
||||||
LOG.info("Certificate URI: " + certificate.getLocation());
|
|
||||||
|
|
||||||
// Download the certificate
|
|
||||||
X509Certificate cert = certificate.download();
|
|
||||||
X509Certificate[] chain = certificate.downloadChain();
|
|
||||||
|
|
||||||
// Write certificate only (e.g. for Apache's SSLCertificateFile)
|
|
||||||
try (FileWriter fw = new FileWriter(DOMAIN_CERT_FILE)) {
|
|
||||||
CertificateUtils.writeX509Certificate(cert, fw);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Write chain only (e.g. for Apache's SSLCertificateChainFile)
|
|
||||||
try (FileWriter fw = new FileWriter(CERT_CHAIN_FILE)) {
|
|
||||||
CertificateUtils.writeX509CertificateChain(fw, null, chain);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Write combined certificate and chain (e.g. for nginx)
|
|
||||||
try (FileWriter fw = new FileWriter(DOMAIN_CHAIN_FILE)) {
|
|
||||||
CertificateUtils.writeX509CertificateChain(fw, cert, chain);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Revoke the certificate (uncomment if needed...)
|
|
||||||
// certificate.revoke();
|
|
||||||
|
|
||||||
// Deactivate the registration (uncomment if needed...)
|
|
||||||
// reg.deactivate();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Prepares HTTP challenge.
|
* Prepares a HTTP challenge.
|
||||||
|
* <p>
|
||||||
|
* The verification of this challenge expects a file with a certain content to be
|
||||||
|
* reachable at a given path under the domain to be tested.
|
||||||
|
* <p>
|
||||||
|
* This example outputs instructions that need to be executed manually. In a
|
||||||
|
* production environment, you would rather generate this file automatically, or maybe
|
||||||
|
* use a servlet that returns {@link Http01Challenge#getAuthorization()}.
|
||||||
|
*
|
||||||
|
* @param auth
|
||||||
|
* {@link Authorization} to find the challenge in
|
||||||
|
* @param domain
|
||||||
|
* Domain name to be authorized
|
||||||
|
* @return {@link Challenge} to verify
|
||||||
*/
|
*/
|
||||||
public Challenge httpChallenge(Authorization auth, String domain) throws AcmeException {
|
public Challenge httpChallenge(Authorization auth, String domain) throws AcmeException {
|
||||||
// Find a single http-01 challenge
|
// Find a single http-01 challenge
|
||||||
Http01Challenge challenge = auth.findChallenge(Http01Challenge.TYPE);
|
Http01Challenge challenge = auth.findChallenge(Http01Challenge.TYPE);
|
||||||
if (challenge == null) {
|
if (challenge == null) {
|
||||||
LOG.error("Found no " + Http01Challenge.TYPE + " challenge, don't know what to do...");
|
throw new AcmeException("Found no " + Http01Challenge.TYPE + " challenge, don't know what to do...");
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Output the challenge, wait for acknowledge...
|
// Output the challenge, wait for acknowledge...
|
||||||
|
@ -234,27 +314,30 @@ public class ClientTest {
|
||||||
message.append("http://").append(domain).append("/.well-known/acme-challenge/").append(challenge.getToken()).append("\n\n");
|
message.append("http://").append(domain).append("/.well-known/acme-challenge/").append(challenge.getToken()).append("\n\n");
|
||||||
message.append("Content:\n\n");
|
message.append("Content:\n\n");
|
||||||
message.append(challenge.getAuthorization());
|
message.append(challenge.getAuthorization());
|
||||||
int option = JOptionPane.showConfirmDialog(null,
|
acceptChallenge(message.toString());
|
||||||
message.toString(),
|
|
||||||
"Prepare Challenge",
|
|
||||||
JOptionPane.OK_CANCEL_OPTION);
|
|
||||||
if (option == JOptionPane.CANCEL_OPTION) {
|
|
||||||
LOG.error("User cancelled challenge");
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return challenge;
|
return challenge;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Prepares DNS challenge.
|
* Prepares a DNS challenge.
|
||||||
|
* <p>
|
||||||
|
* The verification of this challenge expects a TXT record with a certain content.
|
||||||
|
* <p>
|
||||||
|
* This example outputs instructions that need to be executed manually. In a
|
||||||
|
* production environment, you would rather configure your DNS automatically.
|
||||||
|
*
|
||||||
|
* @param auth
|
||||||
|
* {@link Authorization} to find the challenge in
|
||||||
|
* @param domain
|
||||||
|
* Domain name to be authorized
|
||||||
|
* @return {@link Challenge} to verify
|
||||||
*/
|
*/
|
||||||
public Challenge dnsChallenge(Authorization auth, String domain) throws AcmeException {
|
public Challenge dnsChallenge(Authorization auth, String domain) throws AcmeException {
|
||||||
// Find a single dns-01 challenge
|
// Find a single dns-01 challenge
|
||||||
Dns01Challenge challenge = auth.findChallenge(Dns01Challenge.TYPE);
|
Dns01Challenge challenge = auth.findChallenge(Dns01Challenge.TYPE);
|
||||||
if (challenge == null) {
|
if (challenge == null) {
|
||||||
LOG.error("Found no " + Dns01Challenge.TYPE + " challenge, don't know what to do...");
|
throw new AcmeException("Found no " + Dns01Challenge.TYPE + " challenge, don't know what to do...");
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Output the challenge, wait for acknowledge...
|
// Output the challenge, wait for acknowledge...
|
||||||
|
@ -265,50 +348,52 @@ public class ClientTest {
|
||||||
StringBuilder message = new StringBuilder();
|
StringBuilder message = new StringBuilder();
|
||||||
message.append("Please create a TXT record:\n\n");
|
message.append("Please create a TXT record:\n\n");
|
||||||
message.append("_acme-challenge." + domain + ". IN TXT " + challenge.getDigest());
|
message.append("_acme-challenge." + domain + ". IN TXT " + challenge.getDigest());
|
||||||
int option = JOptionPane.showConfirmDialog(null,
|
acceptChallenge(message.toString());
|
||||||
message.toString(),
|
|
||||||
"Prepare Challenge",
|
|
||||||
JOptionPane.OK_CANCEL_OPTION);
|
|
||||||
if (option == JOptionPane.CANCEL_OPTION) {
|
|
||||||
LOG.error("User cancelled challenge");
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return challenge;
|
return challenge;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Prepares TLS-SNI challenge.
|
* Prepares a TLS-SNI challenge.
|
||||||
|
* <p>
|
||||||
|
* The verification of this challenge expects that the web server returns a special
|
||||||
|
* validation certificate.
|
||||||
|
* <p>
|
||||||
|
* This example outputs instructions that need to be executed manually. In a
|
||||||
|
* production environment, you would rather configure your web server automatically.
|
||||||
|
*
|
||||||
|
* @param auth
|
||||||
|
* {@link Authorization} to find the challenge in
|
||||||
|
* @param domain
|
||||||
|
* Domain name to be authorized
|
||||||
|
* @return {@link Challenge} to verify
|
||||||
*/
|
*/
|
||||||
@SuppressWarnings("deprecation") // until tls-sni-02 is supported
|
@SuppressWarnings("deprecation") // until tls-sni-02 is supported
|
||||||
public Challenge tlsSniChallenge(Authorization auth, String domain) throws AcmeException {
|
public Challenge tlsSniChallenge(Authorization auth, String domain) throws AcmeException {
|
||||||
// Find a single tls-sni-01 challenge
|
// Find a single tls-sni-01 challenge
|
||||||
org.shredzone.acme4j.challenge.TlsSni01Challenge challenge = auth.findChallenge(org.shredzone.acme4j.challenge.TlsSni01Challenge.TYPE);
|
org.shredzone.acme4j.challenge.TlsSni01Challenge challenge = auth.findChallenge(org.shredzone.acme4j.challenge.TlsSni01Challenge.TYPE);
|
||||||
if (challenge == null) {
|
if (challenge == null) {
|
||||||
LOG.error("Found no " + org.shredzone.acme4j.challenge.TlsSni01Challenge.TYPE + " challenge, don't know what to do...");
|
throw new AcmeException("Found no " + org.shredzone.acme4j.challenge.TlsSni01Challenge.TYPE + " challenge, don't know what to do...");
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get the Subject
|
// Get the Subject
|
||||||
String subject = challenge.getSubject();
|
String subject = challenge.getSubject();
|
||||||
|
|
||||||
// Create a keypair
|
// Create a validation key pair
|
||||||
KeyPair domainKeyPair;
|
KeyPair domainKeyPair;
|
||||||
try (FileWriter fw = new FileWriter("tlssni.key")) {
|
try (FileWriter fw = new FileWriter("tlssni.key")) {
|
||||||
domainKeyPair = KeyPairUtils.createKeyPair(2048);
|
domainKeyPair = KeyPairUtils.createKeyPair(2048);
|
||||||
KeyPairUtils.writeKeyPair(domainKeyPair, fw);
|
KeyPairUtils.writeKeyPair(domainKeyPair, fw);
|
||||||
} catch (IOException ex) {
|
} catch (IOException ex) {
|
||||||
LOG.error("Could not create keypair", ex);
|
throw new AcmeException("Could not write keypair", ex);
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create a certificate
|
// Create a validation certificate
|
||||||
try (FileWriter fw = new FileWriter("tlssni.crt")) {
|
try (FileWriter fw = new FileWriter("tlssni.crt")) {
|
||||||
X509Certificate cert = CertificateUtils.createTlsSniCertificate(domainKeyPair, subject);
|
X509Certificate cert = CertificateUtils.createTlsSniCertificate(domainKeyPair, subject);
|
||||||
CertificateUtils.writeX509Certificate(cert, fw);
|
CertificateUtils.writeX509Certificate(cert, fw);
|
||||||
} catch (IOException ex) {
|
} catch (IOException ex) {
|
||||||
LOG.error("Could not create certificate", ex);
|
throw new AcmeException("Could not write certificate", ex);
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Output the challenge, wait for acknowledge...
|
// Output the challenge, wait for acknowledge...
|
||||||
|
@ -321,42 +406,57 @@ public class ClientTest {
|
||||||
StringBuilder message = new StringBuilder();
|
StringBuilder message = new StringBuilder();
|
||||||
message.append("Please use 'tlssni.key' and 'tlssni.crt' cert for SNI requests to:\n\n");
|
message.append("Please use 'tlssni.key' and 'tlssni.crt' cert for SNI requests to:\n\n");
|
||||||
message.append("https://").append(subject).append("\n\n");
|
message.append("https://").append(subject).append("\n\n");
|
||||||
int option = JOptionPane.showConfirmDialog(null,
|
acceptChallenge(message.toString());
|
||||||
message.toString(),
|
|
||||||
"Prepare Challenge",
|
|
||||||
JOptionPane.OK_CANCEL_OPTION);
|
|
||||||
if (option == JOptionPane.CANCEL_OPTION) {
|
|
||||||
LOG.error("User cancelled challenge");
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return challenge;
|
return challenge;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Presents the user a link to the Terms of Service, and asks for confirmation.
|
* Presents the instructions for preparing the challenge validation, and waits for
|
||||||
|
* dismissal. If the user cancelled the dialog, an exception is thrown.
|
||||||
|
*
|
||||||
|
* @param message
|
||||||
|
* Instructions to be shown in the dialog
|
||||||
|
*/
|
||||||
|
public void acceptChallenge(String message) throws AcmeException {
|
||||||
|
int option = JOptionPane.showConfirmDialog(null,
|
||||||
|
message,
|
||||||
|
"Prepare Challenge",
|
||||||
|
JOptionPane.OK_CANCEL_OPTION);
|
||||||
|
if (option == JOptionPane.CANCEL_OPTION) {
|
||||||
|
throw new AcmeException("User cancelled the challenge");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Presents the user a link to the Terms of Service, and asks for confirmation. If the
|
||||||
|
* user denies confirmation, an exception is thrown.
|
||||||
*
|
*
|
||||||
* @param reg
|
* @param reg
|
||||||
* {@link Registration} User's registration, containing the Agreement URI
|
* {@link Registration} User's registration
|
||||||
* @return {@code true}: User confirmed, {@code false} user rejected
|
* @param agreement
|
||||||
|
* {@link URI} of the Terms of Service
|
||||||
*/
|
*/
|
||||||
public boolean acceptAgreement(Registration reg, URI agreement)
|
public void acceptAgreement(Registration reg, URI agreement) throws AcmeException {
|
||||||
throws AcmeException {
|
|
||||||
int option = JOptionPane.showConfirmDialog(null,
|
int option = JOptionPane.showConfirmDialog(null,
|
||||||
"Do you accept the Terms of Service?\n\n" + agreement,
|
"Do you accept the Terms of Service?\n\n" + agreement,
|
||||||
"Accept T&C",
|
"Accept ToS",
|
||||||
JOptionPane.YES_NO_OPTION);
|
JOptionPane.YES_NO_OPTION);
|
||||||
if (option == JOptionPane.NO_OPTION) {
|
if (option == JOptionPane.NO_OPTION) {
|
||||||
LOG.error("User did not accept Terms of Service");
|
throw new AcmeException("User did not accept Terms of Service");
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Motify the Registration and accept the agreement
|
||||||
reg.modify().setAgreement(agreement).commit();
|
reg.modify().setAgreement(agreement).commit();
|
||||||
LOG.info("Updated user's ToS");
|
LOG.info("Updated user's ToS");
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Invokes this example.
|
||||||
|
*
|
||||||
|
* @param args
|
||||||
|
* Domains to get a certificate for
|
||||||
|
*/
|
||||||
public static void main(String... args) {
|
public static void main(String... args) {
|
||||||
if (args.length == 0) {
|
if (args.length == 0) {
|
||||||
System.err.println("Usage: ClientTest <domain>...");
|
System.err.println("Usage: ClientTest <domain>...");
|
||||||
|
|
Loading…
Reference in New Issue