Tech and Media Labs
This site uses cookies to improve the user experience.


Acme4J Tutorial

Jakob Jenkov
Last update: 2018-09-16

Acme4j is a Java toolkit that enables you to automate the creation of free TLS / SSL certificates. Acme4J is obtaining the TLS / SSL certificates by communicating with the Let's Encrypt certificate authority (CA). Automating certificate generation is a big advantage over updating certificates manually. When automated you save time, and you can changes certificates more often, reducing the risk of your certificate getting compromised.

You can find Acme4J via the Acme4J GitHub repository.

Acme4J Maven Dependencies

The Java examples in this tutorial were created with Java 8, and Acme4J Client v. 2.1 and Acme4J Utils v. 0.3. Here are the Maven dependencies for the toolkit versions used in the examples:

<dependency>
    <groupId>org.shredzone.acme4j</groupId>
    <artifactId>acme4j-client</artifactId>
    <version>2.1</version>
</dependency>

<dependency>
    <groupId>org.shredzone.acme4j</groupId>
    <artifactId>acme4j-utils</artifactId>
    <version>0.3</version>
</dependency>

Acme4J Phases

Using Acme4J to obtain a certificate from Let's Encrypt requires the following phases.

  1. Create a private key for your Let's Encrypt account.
  2. Create a Let's Encrypt account using the private key generated in the previous phase.
  3. Create a certificate order and send it to Acme4J, to obtain a certificate.

Each of these phases can be executed separate in time from each other. In fact, phase 1 and 2 are normally only executed once. Phase 3 is repeated for each new certificate needed.

Phase 1: Creating a Private Key For Your Let's Encrypt Account

The first step in using Let's Encrypt with Acme4J is to create a private key for your Let's Encrypt account. This is done before you actually create the account. First you create the private key, then you create your Let's Encrypt account using that private key.

Here is a Java class capable of creating a private key for use with Acme4J:

import org.shredzone.acme4j.util.KeyPairUtils;

import java.io.FileWriter;
import java.io.IOException;
import java.security.KeyPair;

public class AccountKeyPairCreation {

    private String certificateFilePath =  null;

    public AccountKeyPairCreation(String certificateFilePath) {
        this.certificateFilePath = certificateFilePath;
    }

    public void execute() throws IOException {
        // Create a private / public key pair to attach to your CA account.
        String keyPairFile = "data/jenkov-com-ca-account-key-pair.pem";
        KeyPair accountKeyPair = createAndSaveKeyPair(keyPairFile);

    }

    private KeyPair createAndSaveKeyPair(String keyPairFile) throws IOException {
        KeyPair accountKeyPair = KeyPairUtils.createKeyPair(2048);

        // Write key to disk, so it can be reused another time.
        try (FileWriter fw = new FileWriter(keyPairFile)) {
            KeyPairUtils.writeKeyPair(accountKeyPair, fw);
        }
        return accountKeyPair;
    }
}

It is the execute() method that creates the private key. The private key is stored in a file, for later use.

You should hold on to this private key. You will need it whenever you interact with your Let's Encrypt account, so don't lose it. Also, don't let anyone get access to it. Anyone with access to your private key can interact with Let's Encrypt on your behalf!

Phase 2: Creating a Let's Encrypt Account

Once you have created a private key for your Let's Encrypt account, you need to create a Let's Encrypt account.

As a result of the account creation process you get an account URL back from Let's Encrypt. You need to store this URL for later use. The account URL is used when ordering certificates in the future.

Here is a class that can create a Let's Encrypt account:

import org.shredzone.acme4j.Account;
import org.shredzone.acme4j.AccountBuilder;
import org.shredzone.acme4j.Session;
import org.shredzone.acme4j.exception.AcmeException;
import org.shredzone.acme4j.util.KeyPairUtils;

import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.net.URL;
import java.security.KeyPair;

public class AccountCreation {

    private String accountKeyPairFilePath = null;
    private String accountUrlFilePath = null;
    private String letsEncryptUrl = null;

    public AccountCreation accountKeyPairFilePath(String filePath){
        this.accountKeyPairFilePath = filePath;
        return this;
    }

    public AccountCreation accountUrlFilePath(String filePath){
        this.accountUrlFilePath = filePath;
        return this;
    }

    public AccountCreation letsEncryptUrl(String url){
        this.letsEncryptUrl = url;
        return this;
    }

    public void execute() throws IOException, AcmeException {
        KeyPair keyPair = KeyPairUtils.readKeyPair(new FileReader(this.accountKeyPairFilePath));

        Session session = new Session(this.letsEncryptUrl);;
        Account account = new AccountBuilder()
                //.onlyExisting()
                .useKeyPair(keyPair)
                .agreeToTermsOfService()
                .create(session)
                ;

        URL accountLocationUrl = account.getLocation();
        try(FileWriter fileWriter = new FileWriter(this.accountUrlFilePath)){
            fileWriter.write(accountLocationUrl.toString());
        }
    }
}

It is the execute() method that starts the account creation process. The class needs 3 inputs to do its work:

  1. The path to the file containing your Let's Encrypt private key.
  2. The path to store the returned account URL in.
  3. The URL to the Let's Encrypt API endpoint.

Here is an example of using the above class:

import com.jenkov.acme4j.commands.AccountCreation;
import org.shredzone.acme4j.exception.AcmeException;

import java.io.IOException;

public class AcmePhase2 {


    /*
        ACME phase 2 is the creation of an account with the CA.
     */
    public static void main(String[] args) throws IOException, AcmeException {

        AccountCreation accountCreation = new AccountCreation()
                .accountKeyPairFilePath ("le-account/account-private-key.pem")
                .accountUrlFilePath     ("le-account/account-url.txt")
                .letsEncryptUrl         ("acme://letsencrypt.org/staging")
                ;

        accountCreation.execute();

    }
}

Phase 3: Creating a Certificate

Phase 3 is the most complicated of the Acme4J phases. Phase 3 does by itself consist of multiple, smaller steps. The steps are listed here:

  1. Create an Order object and configure it.
  2. Process returned authorizations.
  3. Download certificate

These steps are explained in more detail in the following sections.

Creating an Order Object

Phase 3 consists of the following steps in code:

    Order order = account.newOrder()
            .domains(domains)
            .create();

    for (Authorization auth : order.getAuthorizations()) {
        if (auth.getStatus() != Status.VALID) {
            processAuth(auth);
        }
    }

    createCertificateSigningRequest(order, domains);
    downloadCertificate(order);

The first part of phase 3 consists of creating an Order object representing a certificate order. The fully qualified name for the Order class is org.shredzone.acme4j.Order .

Calling create() results in a request being sent to Let's Encrypt. The response is a set of authorizations which you have to process in order to get the certificate issued.

Process Authorizations

Once you have called the order.create() method a request is sent to Let's Encrypt. Let's Encrypt responds with a set of authorizations that you need to process in order to get the requested certificate. The authorization verifies to Let's Encrypt that you actually own the domain you are requesting a certificate for.

There are two primary types of authorizations:

  • HTTP Challenge
  • DNS Challenge

The HTTP challenge consists of Let's Encrypt giving you some data that you need to upload to your web server hosting the given domain. Let's Encrypt will then download that data - and if successful - will consider that as a confirmation that you own (or at least administer) the given domain.

The DNS challenge works similarly, but requires some configuration of your DNS. Let's Encrypt will then check this configuration and use that as verification that you own the domain. I will not be covering the DNS challenge option, but only the HTTP challenge option in this tutorial.

There will be one authorization per domain in your certificate order. You obtain and process the authorizations like this:

for (Authorization auth : order.getAuthorizations()) {
    if (auth.getStatus() != Status.VALID) {
        processAuth(auth);
    }
}

Processing the authorizations is done inside the processAuth(auth) method call. Here is how that method looks:

private void processAuth(Authorization auth) throws AcmeException, InterruptedException {

    Http01Challenge challenge = auth.findChallenge(Http01Challenge.TYPE);

    String fileName    = challenge.getToken());
    String fileContent = challenge.getAuthorization();
    String domain      = challenge.getDomain();

    challenge.trigger();

    while (auth.getStatus() != Status.VALID) {
        Thread.sleep(3000L);
        auth.update();
    }
}

Within the Authorization object (the auth parameter) there are one or more challenges, of which one must be met.

The above example specifically looks for an HTTP authorization object, which is then triggered by calling challenge.trigger(). Before the challenge is triggered, you must read the value from challenge.getAuthorization() and upload it in a file to your web server. The URL the value must be available at, is:

http://${domain}/.well-known/acme-challenge/${token}

... where ${domain} is the domain for which you are requesting the domain for (returned by challenge.getDomain() ), and ${token} is the value returned by challenge.getToken().

In the example above, the value for ${domain} is stored in the variable domain, the value for ${token} in the fileName variable, and the content of the file found at that URL should be the content of the fileContent variable.

If your code cannot access your web server to upload the challenge file before calling challenge.trigger(), you can run the code in your debugger, set a breakpoint before calling challenge.trigger(), pause the code at that breakpoint, read the token (file name) and authorization (file content) out of the challenge object, upload that to your web server, and then continue the code in the debugger.

After the challenge has been triggered, you need to wait until the challenge has been verified by Let's Encrypt. That is what happens in the last part of the processAuth() method. More specifically, it is this block that waits until the challenge is verified:

    while (auth.getStatus() != Status.VALID) {
        Thread.sleep(3000L);
        auth.update();
    }

Creating Certificate Signing Request

Part 2 of phase 3 is to create the actual certificate signing request. Here is how that looks:

private void createCertificateSigningRequest(Order order, String ... domains) throws Exception {
    KeyPair domainKeyPair = KeyPairUtils.createKeyPair(2048);

    CSRBuilder csrb = new CSRBuilder();
    for(String domain : domains){
        csrb.addDomain(domain);
    }
    csrb.setOrganization(this.organization);
    csrb.sign(domainKeyPair);
    byte[] csr = csrb.getEncoded();

    csrb.write(new FileWriter(this.certificateSigningRequestFilePath));

    order.execute(csr);
}

First a domain public / private key pair is generated. This is not the same key pair as you use for your Let's Encrypt account.

Second you add all the domains you want certificates for.

Third you set the organization (your organization) into the certificate signing request.

Fourth you sign the certificate signing request with the domain key pair.

Fifth you write the certificate signing request to disk - just to have it for future reference.

Finally you execute the order. This sends the certificate signing request to Let's Encrypt.

Downloading Certificate

Part 3 of phase 3 is to download the certificate, after having executed the certificate signing order. Here is how that looks in code:

private void downloadCertificate(Order order) throws InterruptedException, AcmeException, IOException {
    while (order.getStatus() != Status.VALID) {
        Thread.sleep(3000L);
        order.update();
    }

    Certificate cert = order.getCertificate();

    try (FileWriter fw = new FileWriter(this.certificateFilePath)) {
        cert.writeCertificate(fw);
    }
}

First, the code above waits until the Order object has status Status.VALID.

Second, the code obtains the newly issued certificate from the Order object.

Third, the certificate is written to a file.

Full Code Example

Here is a full Java code example of how phase 3 could be implemented:

import org.shredzone.acme4j.*;
import org.shredzone.acme4j.challenge.Http01Challenge;
import org.shredzone.acme4j.exception.AcmeException;
import org.shredzone.acme4j.util.CSRBuilder;
import org.shredzone.acme4j.util.KeyPairUtils;

import java.io.FileWriter;
import java.io.IOException;
import java.security.KeyPair;
import java.security.cert.X509Certificate;
import java.time.Duration;
import java.time.Instant;
import java.util.List;

public class OrderCertificate {

    public static void orderCertificate(Account account, Instant validUntil,
          String ... domains)
    throws AcmeException, InterruptedException, IOException {
        Order order = account.newOrder()
                .domains(domains)
                .notAfter(validUntil)
                .create();

        for (Authorization auth : order.getAuthorizations()) {
            if (auth.getStatus() != Status.VALID) {
                processAuth(auth);
            }
        }

        createCertificateSigningRequest(order, domains);

        downloadCertificate(order);
    }

    private static void downloadCertificate(Order order)
    throws InterruptedException, AcmeException, IOException {
        while (order.getStatus() != Status.VALID) {
            Thread.sleep(3000L);
            order.update();
        }

        Certificate cert = order.getCertificate();

        try (FileWriter fw = new FileWriter("jenkov-com-cert-chain.crt")) {
            cert.writeCertificate(fw);
        }
    }

    private static void createCertificateSigningRequest(Order order, String ... domains)
    throws IOException, AcmeException {
        KeyPair domainKeyPair = KeyPairUtils.createKeyPair(2048);

        CSRBuilder csrb = new CSRBuilder();
        for(String domain : domains){
            csrb.addDomain(domain);
    }
        csrb.setOrganization("Jenkov Aps");
        csrb.sign(domainKeyPair);
        byte[] csr = csrb.getEncoded();

        csrb.write(new FileWriter("example.csr"));

        order.execute(csr);
    }

    private static void processAuth(Authorization auth) throws AcmeException,
            InterruptedException {
        Http01Challenge challenge = auth.findChallenge(Http01Challenge.TYPE);

        challenge.trigger();

        while (auth.getStatus() != Status.VALID) {
            Thread.sleep(3000L);
            auth.update();
        }
    }


}

Using the Issued Certificate

Once you have downloaded the newly issued SSL / TLS certificate, you need to use it. Exactly how you use it depends on the web server you are using, and is therefore out of scope of this Acme4J tutorial.

Note: Several Java web servers will require the certificate to be imported into a Java Keystore. When I find out exactly how to do this, I will update this tutorial with that information. If you have that information, I would appreciate if you would share it with me!

Jakob Jenkov




Copyright  Jenkov Aps
Close TOC