Certificate Ordering

Once you have your account set up, you are ready to order certificates.

Creating an Order

Use Account.newOrder() to start ordering a new certificate. It returns an OrderBuilder object that helps you to collect the parameters of the order. You can give one or more domain names. Optionally you can also give your desired notBefore and notAfter dates for the generated certificate, but it is at the discretion of the CA to use (or ignore) these values.

Account account = ... // your Account object

Order order = account.newOrder()
        .domains("example.org", "www.example.org", "m.example.org")
        .notAfter(Instant.now().plus(Duration.ofDays(20L)))     // optional
        .create();

Note

The number of domains per certificate may be limited. See your CA's documentation for the limits.

Authorization

The Order resource contains a collection of Authorization objects that can be read from the getAuthorizations() method.

Each Authorization is associated with one of the domains in your order. Authorization.getIdentifier() returns that identifier. Before you can retrieve your certificate, you must process all authorizations that are in a PENDING state.

for (Authorization auth : order.getAuthorizations()) {
  if (auth.getStatus() == Status.PENDING) {
    log.info("Authorizing " + auth.getIdentifier());

    // process auth by performing a challenge, see below
     :
     :
  }
}

If all Authorization objects are in status VALID, you are ready to finalize your order.

Challenge

The Authorization instance contains further details about how you can prove the ownership of your domain. An ACME server offers one or more authorization methods, called Challenge.

Authorization.getChallenges() returns a collection of all Challenges offered by the CA for domain ownership validation. You only need to complete one of them to successfully authorize your domain. You would usually pick the challenge that is best suited for your infrastructure.

Tip

See here for a description of all standard challenges. However, your CA may not offer all of the standard types, and may offer additional, proprietary challenge types.

The simplest way is to invoke findChallenge(), stating the challenge type your system is able to provide (either as challenge name or challenge class type):

Optional<Http01Challenge> challenge = auth.findChallenge(Http01Challenge.TYPE); // by name
Optional<Http01Challenge> challenge = auth.findChallenge(Http01Challenge.class); // by type

It returns a properly casted Challenge object, or empty if your challenge type was not offered by the CA. In this example, your system choses Http01Challenge because it is able to respond to a http-01 challenge.

Tip

Passing the challenge type is preferred over the challenge name, as type checks are performed at compile time here. Passing in the challenge name might result in a ClassCastException at runtime.

The returned Challenge resource provides all the data that is necessary for a successful verification of your domain ownership (see the documentation of the individual challenges).

After you have performed the necessary steps to set up the response to the challenge (e.g. configuring your web server or modifying your DNS records), you tell the ACME server that you are ready for validation:

challenge.trigger();

Now you have to wait for the server to check your response. If the checks are completed, the CA will set the authorization status to VALID or INVALID. The easiest (but admittedly also the ugliest) way is to poll the status:

while (!EnumSet.of(Status.VALID, Status.INVALID).contains(auth.getStatus())) {
  Thread.sleep(3000L);
  auth.fetch();
}

This is a very simple example which can be improved in many ways:

  • Limit the number of checks, to avoid endless loops if an authorization is stuck on server side.
  • Wait with the status checks until the CA has accessed the response for the first time (e.g. after an incoming HTTP request to the response file).
  • Use an asynchronous architecture instead of a blocking Thread.sleep().
  • Check if auth.fetch() returns a retry-after Instant, and wait for the next update at least until this moment is reached. See the example for a simple way to do that.

The CA server may start with the validation immediately after trigger() is invoked, so make sure your server is ready to respond to requests before invoking trigger(). Otherwise the challenge might fail instantly.

Also keep your response available until the status has changed to VALID or INVALID. The ACME server may check your response multiple times, and from different IPs! If the status gets VALID or INVALID, the response you have set up before is not needed anymore. It can (and should) be removed.

Tip

A common mistake is that the server infrastructure is not completely ready when trigger() is invoked (e.g. caches are not purged, services are still restarting, synchronization between instances is still in progress). Also, do not tear down the challenge response too early, as the CA might perform multiple checks.

If your authorization status turned to VALID, you have successfully authorized your domain, and you are ready for the next step.

Finalizing the Order

After successfully completing all authorizations, the order needs to be finalized.

First of all, you will need to generate a key pair that is used for certification and encryption of the domain. Similar to the account key pair, you can either use external tool, Java's own crypto framework, or use the KeyPairUtils.

Tip

Never use your account key pair as domain key pair, but always generate separate key pairs!

After that, the order can be finalized:

KeyPair domainKeyPair = ... // KeyPair to be used for HTTPS encryption

order.execute(domainKeyPair);

acme4j will automatically take care of creating a minimal CSR for this order internally. If you need to expand this CSR (e.g. with your company name), you can do so:

order.execute(domainKeyPair, csr -> {
    csr.setOrganization("ACME Corp.");
});

It depends on the CA if other CSR properties (like Organization, Organization Unit) are accepted. Some may even require these properties to be set, while others may ignore them when generating the certificate.

You can also create a custom CSR, and pass it to the order with either execute(PKCS10CertificationRequest csr) or execute(byte[] csr).

Note

According to RFC-8555, the correct technical term is finalization of an order. However, Java has a method called Object.finalize() which is problematic and should not be used. To avoid confusion with that method, the finalization methods are intentionally called execute in acme4j.

Retrieving the Certificate

Once you completed all the previous steps, it is finally time to download the signed certificate.

But first we need to wait until the certificate is available for download. Again, a primitive way is to poll the status:

Order order = ... // your Order object from the previous step

while (!EnumSet.of(Status.VALID, Status.INVALID).contains(order.getStatus())) {
  Thread.sleep(3000L);
  order.fetch();
}

This is a very simple example which can be improved in many ways:

  • Limit the number of checks, to avoid endless loops if the order is stuck on server side.
  • Use an asynchronous architecture instead of a blocking Thread.sleep().
  • Check if order.fetch() returns a retry-after Instant, and wait for the next update at least until this moment is reached. See the example for a simple way to do that.

Tip

If the status is PENDING, you have not completed all authorizations yet.

Note

Always check the status before downloading the certificate, even if it seems that the CA sets the status to VALID immediately.

As soon as the status turns VALID, you can retrieve a Certificate object:

Certificate cert = order.getCertificate();

The Certificate object offers methods to get the certificate or the certificate chain.

X509Certificate cert = cert.getCertificate();
List<X509Certificate> chain = cert.getCertificateChain();

cert only contains your leaf certificate. However, most servers require the certificate chain that also contains all intermediate certificates up to the root CA.

You can write the certificate chain to disk using the Certificate.writeCertificate() method. It will create a .crt file that is accepted by most servers (like Apache, nginx, postfix, dovecot, etc.).

Congratulations! You have just created your first certificate via acme4j.

List all Orders

To get a list of all current orders of your account, invoke Account.getOrders().

Note that for reasons lying in the ACME protocol, the result is an Iterator<Order> and not a list. Also, any invocation of Iterator.next() can initiate a network call to the CA, and may throw an AcmeProtocolException if there was an error.

Important

This method is a mandatory part of RFC-8555. Still, as of now, this functionality has not been implemented in all Boulder based CAs (like Let's Encrypt) and will throw an AcmeNotSupportedException. Also see this issue. At the moment, the only workaround is to store Order location URLs (or other resource URLs) locally along with the certificates, see the Resources and Persistence chapter.

Wildcard Certificates

If supported by the CA, you can also generate a wildcard certificate that is valid for all subdomains of a domain, by prefixing the domain name with *. (e.g. *.example.org). The domain itself is not covered by the wildcard certificate and also needs to be added to the order if necessary.

Note

acme4j accepts all kind of wildcard notations (e.g. www.*.example.org, *.*.example.org). However, those notations are not specified. They may be rejected by the CA, or may not work as expected.

You must be able to prove ownership of the domain that you want to order a wildcard certificate for (i.e. for *.example.org ownership of example.org needs to be proven). The corresponding Authorization resource only refers to that domain, and does not contain the wildcard notation. However, the Authorization.isWildcard() method will reveal that this authorization is related to a wildcard certificate.

The following example creates an Order for example.org and *.example.org:

KeyPair domainKeyPair = ... // KeyPair to be used for HTTPS encryption

Order order = account.newOrder()
        .domains("example.org", "*.example.org")
        .create();

order.execute(domainKeyPair);

In the subsequent authorization process, you would only have to prove ownership of the example.org domain.

Note

Some CAs may reject wildcard certificate orders at all, may only offer a limited set of challenge types, or may require special challenge types that are not documented here. Refer to your CA's documentation to find out about the wildcard certificate policy.

IP Identifiers

Besides domains, acme4j also supports IP identifier validation as specified in RFC 8738. If your CA offers ACME IP support, you can add IP Identifier objects to the order:

Order order = account.newOrder()
        .identifier(Identifier.ip(InetAddress.getByName("192.168.1.2")))
        .identifier(Identifier.ip("192.168.2.3"))   // for your convenience
        .identifier(Identifier.dns("example.org"))
        .create();

The example also shows how to add domain names as DNS Identifier objects. Adding domain names via domain() is just a shortcut notation for it.