Make the example universal and CA neutral

I like to avoid having different examples for different CAs or
scenarios, as it takes unnecessary time to keep them in sync and
updated.

For this reason, I merged both examples back in a single example again,
which now also handles EAB if necessary.

I also used a generic example CA (example.org) so no CA is favored in
the source code. The desired connection URI must now be configured
first, in order to make the example run.

The documentation was updated accordingly. Rationale is that I don't
want the documentation to be cluttered with all possible CAs, so none of
them is favored now.
pull/168/head
Richard Körber 2024-02-26 18:19:26 +01:00
parent 7c17645212
commit f2ae26b822
No known key found for this signature in database
GPG Key ID: AAB9FD19C78AA3E0
3 changed files with 103 additions and 522 deletions

View File

@ -53,27 +53,55 @@ import org.slf4j.LoggerFactory;
/**
* A simple client test tool.
* <p>
* Pass the names of the domains as parameters.
* First check the configuration constants at the top of the class. Then run the class,
* and pass in the names of the domains as parameters.
* <p>
* The tool won't run as-is. You MUST change the {@link #CA_URI} constant and set the
* connection URI of your target CA there.
* <p>
* If your CA requires External Account Binding (EAB), you MUST also fill the
* {@link #EAB_KID} and {@link #EAB_HMAC} constants with the values provided by your CA.
* <p>
* If your CA requires an email field to be set in your account, you also need to set
* {@link #ACCOUNT_EMAIL}.
* <p>
* All other fields are optional and should work with the default values, unless your CA
* has special requirements (e.g. to the key type).
*
* @see <a href="https://shredzone.org/maven/acme4j/example.html">This example, fully
* explained in the documentation.</a>
*/
public class ClientTest {
// Set the Connection URI of your CA here. For testing purposes, use a staging
// server if possible. Example: "acme://letsencrypt.org/staging" for the Let's
// Encrypt staging server.
private static final String CA_URI = "acme://example.com/staging";
// E-Mail address to be associated with the account. Optional, null if not used.
private static final String ACCOUNT_EMAIL = null;
// If the CA requires External Account Binding (EAB), set the provided KID and HMAC here.
private static final String EAB_KID = null;
private static final String EAB_HMAC = null;
// A supplier for a new account KeyPair. The default creates a new EC key pair.
private static Supplier<KeyPair> ACCOUNT_KEY_SUPPLIER = () -> KeyPairUtils.createKeyPair();
// A supplier for a new domain KeyPair. The default creates a RSA key pair.
private static Supplier<KeyPair> DOMAIN_KEY_SUPPLIER = () -> KeyPairUtils.createKeyPair(4096);
// File name of the User Key Pair
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");
// File name of the 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;
// Maximum attempts of status polling until VALID/INVALID is expected
private static final int MAX_ATTEMPTS = 50;
@ -92,9 +120,8 @@ public class ClientTest {
// Load the user key file. If there is no key file, create a new one.
KeyPair userKeyPair = loadOrCreateUserKeyPair();
// Create a session for Let's Encrypt.
// Use "acme://letsencrypt.org" for production server
Session session = new Session("acme://letsencrypt.org/staging");
// Create a session.
Session session = new Session(CA_URI);
// Get the Account.
// If there is no account yet, create a new one.
@ -157,7 +184,7 @@ public class ClientTest {
} else {
// If there is none, create a new key pair and save it
KeyPair userKeyPair = KeyPairUtils.createKeyPair(KEY_SIZE);
KeyPair userKeyPair = ACCOUNT_KEY_SUPPLIER.get();
try (FileWriter fw = new FileWriter(USER_KEY_FILE)) {
KeyPairUtils.writeKeyPair(userKeyPair, fw);
}
@ -177,7 +204,7 @@ public class ClientTest {
return KeyPairUtils.readKeyPair(fr);
}
} else {
KeyPair domainKeyPair = KeyPairUtils.createKeyPair(KEY_SIZE);
KeyPair domainKeyPair = DOMAIN_KEY_SUPPLIER.get();
try (FileWriter fw = new FileWriter(DOMAIN_KEY_FILE)) {
KeyPairUtils.writeKeyPair(domainKeyPair, fw);
}
@ -206,10 +233,21 @@ public class ClientTest {
acceptAgreement(tos.get());
}
Account account = new AccountBuilder()
AccountBuilder accountBuilder = new AccountBuilder()
.agreeToTermsOfService()
.useKeyPair(accountKey)
.create(session);
.useKeyPair(accountKey);
// Set your email (if available)
if (ACCOUNT_EMAIL != null) {
accountBuilder.addEmail(ACCOUNT_EMAIL);
}
// Use the KID and HMAC if the CA uses External Account Binding
if (EAB_KID != null && EAB_HMAC != null) {
accountBuilder.withKeyIdentifier(EAB_KID, EAB_HMAC);
}
Account account = accountBuilder.create(session);
LOG.info("Registered a new user, URL: {}", account.getLocation());
return account;

View File

@ -1,455 +0,0 @@
/*
* acme4j - Java ACME client
*
* Copyright (C) 2015 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.example;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.shredzone.acme4j.*;
import org.shredzone.acme4j.challenge.Challenge;
import org.shredzone.acme4j.challenge.Dns01Challenge;
import org.shredzone.acme4j.challenge.Http01Challenge;
import org.shredzone.acme4j.exception.AcmeException;
import org.shredzone.acme4j.util.KeyPairUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.swing.*;
import java.io.File;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.net.URI;
import java.net.URL;
import java.security.KeyPair;
import java.security.Security;
import java.util.Arrays;
import java.util.Collection;
import java.util.Optional;
/**
* A simple client test tool.
* <p>
* Pass the names of the domains as parameters.
*/
public class SSLClientWithEabTest {
// File name of the User Key Pair
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");
// 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 Logger LOG = LoggerFactory.getLogger(SSLClientWithEabTest.class);
private enum ChallengeType {HTTP, DNS}
/**
* Generates a certificate for the given domains. Also takes care for the registration
* process.
*
* @param domains
* Domains to get a common certificate for
* @param eabKid
* Value of --eab-kid
* @param eabHmacKey
* Value of --eab-hmac-key
* @param emailAddress
* Email address of account that owns the key information
*/
public void fetchCertificate(Collection<String> domains, String eabKid, String eabHmacKey, String emailAddress) throws IOException, AcmeException {
// Load the user key file. If there is no key file, create a new one.
KeyPair userKeyPair = loadOrCreateUserKeyPair();
// Create a session for SSL.com.
// Use "acme://ssl.com" for production server
Session session = new Session("acme://ssl.com/staging");
// Get the Account.
// If there is no account yet, create a new one.
Account acct = findOrRegisterAccount(session, userKeyPair, eabKid, eabHmacKey, emailAddress);
// Load or create a key pair for the domains. This should not be the userKeyPair!
KeyPair domainKeyPair = loadOrCreateDomainKeyPair();
// Order the certificate
Order order = acct.newOrder().domains(domains).create();
// Perform all required authorizations
for (Authorization auth : order.getAuthorizations()) {
authorize(auth);
}
// Order the certificate
order.execute(domainKeyPair);
// Wait for the order to complete
try {
int attempts = 10;
while (order.getStatus() != Status.VALID && attempts-- > 0) {
// Did the order fail?
if (order.getStatus() == Status.INVALID) {
LOG.error("Order has failed, reason: {}", order.getError()
.map(Problem::toString)
.orElse("unknown")
);
throw new AcmeException("Order failed... Giving up.");
}
// Wait for a few seconds
Thread.sleep(3000L);
// Then update the status
order.update();
}
} catch (InterruptedException ex) {
LOG.error("interrupted", ex);
Thread.currentThread().interrupt();
}
// Get the certificate
Certificate certificate = order.getCertificate();
LOG.info("Success! The certificate for domains {} has been generated!", domains);
LOG.info("Certificate URL: {}", certificate.getLocation());
// Write a combined file containing the certificate and chain.
try (FileWriter fw = new FileWriter(DOMAIN_CHAIN_FILE)) {
certificate.writeCertificate(fw);
}
// That's all! Configure your web server to use the DOMAIN_KEY_FILE and
// DOMAIN_CHAIN_FILE for the requested domains.
}
/**
* Loads a user key pair from {@link #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 {@link #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();
try (FileWriter fw = new FileWriter(DOMAIN_KEY_FILE)) {
KeyPairUtils.writeKeyPair(domainKeyPair, fw);
}
return domainKeyPair;
}
}
/**
* Finds your {@link Account} 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 account will be
* created.
* <p>
* This is a simple way of finding your {@link Account}. A better way is to get the
* URL of your new account with {@link Account#getLocation()} and store it somewhere.
* If you need to get access to your account later, reconnect to it via {@link
* Session#login(URL, KeyPair)} by using the stored location.
*
* @param session
* {@link Session} to bind with
* @param eabKid
* Value of --eab-kid
* @param eabHmacKey
* Value of --eab-hmac-key
* @param emailAddress
* Email address of account that owns the key information
* @return {@link Account}
*/
private Account findOrRegisterAccount(Session session, KeyPair accountKey, String eabKid, String eabHmacKey, String emailAddress) throws AcmeException {
// Ask the user to accept the TOS, if server provides us with a link.
Optional<URI> tos = session.getMetadata().getTermsOfService();
if (tos.isPresent()) {
acceptAgreement(tos.get());
}
AccountBuilder accountBuilder = new AccountBuilder();
if (eabKid != null && eabHmacKey != null && emailAddress != null) {
accountBuilder = accountBuilder.withKeyIdentifier(eabKid, eabHmacKey)
.addEmail(emailAddress);
}
Account account = accountBuilder
.agreeToTermsOfService()
.useKeyPair(accountKey)
.create(session);
LOG.info("Registered a new user, URL: {}", account.getLocation());
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 auth
* {@link Authorization} to perform
*/
private void authorize(Authorization auth) throws AcmeException {
LOG.info("Authorization for domain {}", auth.getIdentifier().getDomain());
// The authorization is already valid. No need to process a challenge.
if (auth.getStatus() == Status.VALID) {
return;
}
// Find the desired challenge and prepare it.
Challenge challenge = null;
switch (CHALLENGE_TYPE) {
case HTTP:
challenge = httpChallenge(auth);
break;
case DNS:
challenge = dnsChallenge(auth);
break;
}
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;
}
// Now trigger the challenge.
challenge.trigger();
// Poll for the challenge to complete.
try {
int attempts = 10;
while (challenge.getStatus() != Status.VALID && attempts-- > 0) {
// Did the authorization fail?
if (challenge.getStatus() == Status.INVALID) {
LOG.error("Challenge has failed, reason: {}", challenge.getError()
.map(Problem::toString)
.orElse("unknown"));
throw new AcmeException("Challenge failed... Giving up.");
}
// Wait for a few seconds
Thread.sleep(3000L);
// Then update the status
challenge.update();
}
} catch (InterruptedException ex) {
LOG.error("interrupted", ex);
Thread.currentThread().interrupt();
}
// 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 "
+ auth.getIdentifier().getDomain() + ", ... Giving up.");
}
LOG.info("Challenge has been completed. Remember to remove the validation resource.");
completeChallenge("Challenge has been completed.\nYou can remove the resource again now.");
}
/**
* 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
* @return {@link Challenge} to verify
*/
public Challenge httpChallenge(Authorization auth) throws AcmeException {
// Find a single http-01 challenge
Http01Challenge challenge = auth.findChallenge(Http01Challenge.class)
.orElseThrow(() -> new AcmeException("Found no " + Http01Challenge.TYPE
+ " challenge, don't know what to do..."));
// Output the challenge, wait for acknowledge...
LOG.info("Please create a file in your web server's base directory.");
LOG.info("It must be reachable at: http://{}/.well-known/acme-challenge/{}",
auth.getIdentifier().getDomain(), challenge.getToken());
LOG.info("File name: {}", challenge.getToken());
LOG.info("Content: {}", challenge.getAuthorization());
LOG.info("The file must not contain any leading or trailing whitespaces or line breaks!");
LOG.info("If you're ready, dismiss the dialog...");
StringBuilder message = new StringBuilder();
message.append("Please create a file in your web server's base directory.\n\n");
message.append("http://")
.append(auth.getIdentifier().getDomain())
.append("/.well-known/acme-challenge/")
.append(challenge.getToken())
.append("\n\n");
message.append("Content:\n\n");
message.append(challenge.getAuthorization());
acceptChallenge(message.toString());
return 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
* @return {@link Challenge} to verify
*/
public Challenge dnsChallenge(Authorization auth) throws AcmeException {
// Find a single dns-01 challenge
Dns01Challenge challenge = auth.findChallenge(Dns01Challenge.TYPE)
.map(Dns01Challenge.class::cast)
.orElseThrow(() -> new AcmeException("Found no " + Dns01Challenge.TYPE
+ " challenge, don't know what to do..."));
// Output the challenge, wait for acknowledge...
LOG.info("Please create a TXT record:");
LOG.info("{} IN TXT {}",
Dns01Challenge.toRRName(auth.getIdentifier()), challenge.getDigest());
LOG.info("If you're ready, dismiss the dialog...");
StringBuilder message = new StringBuilder();
message.append("Please create a TXT record:\n\n");
message.append(Dns01Challenge.toRRName(auth.getIdentifier()))
.append(" IN TXT ")
.append(challenge.getDigest());
acceptChallenge(message.toString());
return challenge;
}
/**
* 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 instructions for removing the challenge validation, and waits for
* dismissal.
*
* @param message
* Instructions to be shown in the dialog
*/
public void completeChallenge(String message) {
JOptionPane.showMessageDialog(null,
message,
"Complete Challenge",
JOptionPane.INFORMATION_MESSAGE);
}
/**
* Presents the user a link to the Terms of Service, and asks for confirmation. If the
* user denies confirmation, an exception is thrown.
*
* @param agreement
* {@link URI} of the Terms of Service
*/
public void acceptAgreement(URI agreement) throws AcmeException {
int option = JOptionPane.showConfirmDialog(null,
"Do you accept the Terms of Service?\n\n" + agreement,
"Accept ToS",
JOptionPane.YES_NO_OPTION);
if (option == JOptionPane.NO_OPTION) {
throw new AcmeException("User did not accept Terms of Service");
}
}
/**
* Invokes this example.
*
* @param args
* Domains to get a certificate for
*/
public static void main(String... args) {
if (args.length == 0) {
System.err.println("Usage: ClientTest <domain,domain,...> <eab-kid>(optional) <eab-hmac-key>(optional) <account-email>(optional)");
System.exit(1);
}
LOG.info("Starting up...");
Security.addProvider(new BouncyCastleProvider());
Collection<String> domains = Arrays.asList(args[0].split(","));
String eabKid = args.length > 1 ? args[1] : null;
String eabHmacKey = args.length > 2 ? args[2] : null;
String emailAddress = args.length > 3 ? args[3] : null;
try {
SSLClientWithEabTest ct = new SSLClientWithEabTest();
ct.fetchCertificate(domains, eabKid, eabHmacKey, emailAddress);
} catch (Exception ex) {
LOG.error("Failed to get a certificate for domains " + domains, ex);
}
}
}

View File

@ -16,16 +16,37 @@ This chapter contains a copy of the class file, along with explanations about wh
- To make the example easier to understand, I will use the specific datatypes instead of the `var` keyword.
## Configuration
**The example won't run as-is.** You first need to set some constants according to the CA you intend to connect to. All constants can be found at the top of the class.
There is one constant that you **must** change in order to make the example work at all:
* `CA_URI`: Set this constant to the connection URI of the CA you intend to use, see the [Connecting](usage/connecting.md) chapter. (The default value is just an example placeholder and won't work.)
Depending on the requirements of your CA, you might also need to set these constants:
* `ACCOUNT_EMAIL`: This is the email address that is connected to your account. The default is `null`, meaning that no email address is set. Some CAs accept that, but otherwise you can set your email address here.
* `EAB_KID`, `EAB_HMAC`: If your CA requires External Account Binding (EAB), it will provide you with a KID and a HMAC pair that is connected to your account. In this case, you must provide both values in the corresponding constants (be careful not to mix them up). Otherwise both constants must be set to `null`, which is the default and disables EAB.
The other constants should work with their default values, but can still be changed if necessary:
* `ACCOUNT_KEY_SUPPLIER`: A function for generating a new account key pair. The default generates an EC key pair, but you can also use other `KeyPairUtils` methods (or entirely different means) to generate other kind of key pairs.
* `DOMAIN_KEY_SUPPLIER`: A function for generating a new domain key pair. The default generates an RSA key pair, but you can also use other `KeyPairUtils` methods (or entirely different means) to generate other kind of key pairs.
* `USER_KEY_FILE`: File name where the generated account key is stored. Default is `user.key`.
* `DOMAIN_KEY_FILE`: File name where the generated domain key is stored. Default is `domain.key`.
* `DOMAIN_CHAIN_FILE`: File name where the ordered domain certificate chain is stored. Default is `domain-chain.crt`.
* `CHALLENGE_TYPE`: The challenge type you want to perform for domain validation. The default is `ChallengeType.HTTP` for [http-01](challenge/http-01.md) validation, but you can also use `ChallengeType.DNS` to perform a [dns-01](challenge/dns-01.md) validation. The example does not support other kind of challenges.
* `MAX_ATTEMPTS`: Maximum number of poll attempts until a status poll is aborted.
## Running the Example
You can run the `ClientTest` class in your IDE, giving the domain names to be registered as parameters. When changing into the `acme4j-example` directory, the test client can also be invoked via maven in a command line:
After configuration, you can run the `ClientTest` class in your IDE, giving the domain names to be registered as parameters. When changing into the `acme4j-example` directory, the test client can also be invoked via maven in a command line:
```sh
mvn exec:java -Dexec.args="example.com example.org"
```
It is safe to run the example in the default configuration. The domains will be registered with the _Let's Encrypt staging server_ via HTTP challenges. The generated certificates are test certificates that are not suited for production use, as they will be rejected by all standard browsers.
## Invocation
The `main()` method performs a simple parameter check, and then invokes the `ClientTest.fetchCertificate()` method, giving a collection of domain names to get a certificate for.
@ -64,9 +85,8 @@ public void fetchCertificate(Collection<String> domains)
// Load the user key file. If there is no key file, create a new one.
KeyPair userKeyPair = loadOrCreateUserKeyPair();
// Create a session for Let's Encrypt.
// Use "acme://letsencrypt.org" for production server
Session session = new Session("acme://letsencrypt.org/staging");
// Create a session.
Session session = new Session(CA_URI);
// Get the Account.
// If there is no account yet, create a new one.
@ -118,8 +138,6 @@ When this method returned successfully, you will find the domain key pair in a f
If no account was registered with the CA yet, there will also be a new file called `user.key`, which is your account key pair.
The `domain.csr` file contains the CSR that was used for the cerficiate order. It is just written for example purposes, and will not be needed later again. When the certificate is going to be renewed, a new CSR will be generated.
## Creating Key Pairs
There are two sets of key pairs. One is required for creating and accessing your account, the other is required for encrypting the traffic on your domain(s). Even though it is technically possible to use a common key pair for everything, you are strongly encouraged to use separate key pairs for your account and for each of your certificates.
@ -127,7 +145,7 @@ There are two sets of key pairs. One is required for creating and accessing your
A first helper method looks for a file that is called `user.key`. It will contain the key pair that is required for accessing your account. If there is no such key pair, a new one is generated.
!!! important
Backup this key pair in a safe place, as you will be locked out from your account if you should ever lose it! There is no way to recover a lost key pair, or regain access to your account when the key is lost.
Backup this key pair in a safe place, as you will be locked out from your account if you should ever lose it! There may be no way to recover a lost key pair or regain access to your account if the key is lost.
```java
private KeyPair loadOrCreateUserKeyPair() throws IOException {
@ -139,7 +157,7 @@ private KeyPair loadOrCreateUserKeyPair() throws IOException {
} else {
// If there is none, create a new key pair and save it
KeyPair userKeyPair = KeyPairUtils.createKeyPair(KEY_SIZE);
KeyPair userKeyPair = ACCOUNT_KEY_SUPPLIER.get();
try (FileWriter fw = new FileWriter(USER_KEY_FILE)) {
KeyPairUtils.writeKeyPair(userKeyPair, fw);
}
@ -157,7 +175,7 @@ private KeyPair loadOrCreateDomainKeyPair() throws IOException {
return KeyPairUtils.readKeyPair(fr);
}
} else {
KeyPair domainKeyPair = KeyPairUtils.createKeyPair(KEY_SIZE);
KeyPair domainKeyPair = DOMAIN_KEY_SUPPLIER.get();
try (FileWriter fw = new FileWriter(DOMAIN_KEY_FILE)) {
KeyPairUtils.writeKeyPair(domainKeyPair, fw);
}
@ -166,11 +184,11 @@ private KeyPair loadOrCreateDomainKeyPair() throws IOException {
}
```
Both the user and domain key pairs are of the same type and strength in this example. This is not required though. You can mix RSA and EC keys of different strengths.
## Registering an Account
If you does not have an account set up already, you need to create one first. The following method will show a link to the terms of service and ask you to accept it. After that, the `AccountBuilder` will create an account using the given account `KeyPair`.
If you does not have an account set up already, you need to create one first. The following method will show a link to the terms of service and ask you to accept it.
After that, the `AccountBuilder` will create an account using the given account `KeyPair`. It will set an email address if provided. If the CA performs External Account Binding and a KID and HMAC is provided, it is forwarded to the CA.
If your `KeyPair` has already been registered with the CA, no new account will be created, but your existing account will be used.
@ -182,10 +200,21 @@ private Account findOrRegisterAccount(Session session, KeyPair accountKey) throw
acceptAgreement(tos.get());
}
Account account = new AccountBuilder()
AccountBuilder accountBuilder = new AccountBuilder()
.agreeToTermsOfService()
.useKeyPair(accountKey)
.create(session);
.useKeyPair(accountKey);
// Set your email (if available)
if (ACCOUNT_EMAIL != null) {
accountBuilder.addEmail(ACCOUNT_EMAIL);
}
// Use the KID and HMAC if the CA uses External Account Binding
if (EAB_KID != null && EAB_HMAC != null) {
accountBuilder.withKeyIdentifier(EAB_KID, EAB_HMAC);
}
Account account = accountBuilder.create(session);
LOG.info("Registered a new user, URL: {}", account.getLocation());
return account;
@ -244,13 +273,6 @@ private void authorize(Authorization auth)
throw new AcmeException("Challenge failed... Giving up.");
}
// 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 "
+ auth.getIdentifier().getDomain() + ", ... Giving up.");
}
LOG.info("Challenge has been completed. Remember to remove the validation resource.");
completeChallenge("Challenge has been completed.\nYou can remove the resource again now.");
}
@ -334,7 +356,7 @@ public Challenge dnsChallenge(Authorization auth) throws AcmeException {
Make sure that the `TXT` record is actually available before confirming the dialog. The CA may verify the challenge immediately after it was triggered. The challenge will then fail if your DNS server was not ready yet. Depending on your hosting provider, a DNS update may take several minutes until completed.
!!! note
For security reasons, the DNS challenge is mandatory for creating wildcard certificates.
For security reasons, the DNS challenge is mandatory for creating wildcard certificates. This is a restriction of the CA, and not imposed by _acme4j_.
## Checking the Status
@ -396,6 +418,9 @@ private interface UpdateMethod {
}
```
!!! note
Some CAs might provide a `Retry-After` even if the resource has reached a terminal state. For this reason, always check the status _before_ waiting for the recommended time, and leave the loop if a terminal status has been reached.
## User Interaction
In order to keep the example simple, Swing `JOptionPane` dialogs are used for user communication. If the user rejects a dialog, an exception is thrown and the example client is aborted.
@ -428,30 +453,3 @@ public void acceptAgreement(URI agreement) throws AcmeException {
}
}
```
## Constants
These are the default values of the constants used in this example. Feel free to change them as necessary.
```java
// File name of the User Key Pair
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");
// File name of the 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;
// Maximum attempts of status polling until VALID/INVALID is expected
private static final int MAX_ATTEMPTS = 50;
```