WildFly Elytron

Elytron's ACME Client Implementation

The Automated Certificate Management Environment (ACME) protocol became an IETF standard a little over a year ago. This protocol makes it possible to automate the process of obtaining signed certificates from a certificate authority without the need for human intervention. The WildFly Elytron project provides a Java ACME client SPI that has been integrated in WildFly for quite some time now, making it possible to automatically obtain and manage signed certificates from any certificate authority that implements the ACME protocol using WildFly CLI commands. It is also possible to integrate Elytron’s ACME client SPI in other web server environments. In this blog post, we’re going to take a closer look at this SPI and how to make use of it.

Note: A discussion has been started on possibly moving Elytron’s ACME Client SPI to a new SmallRye project in case other projects find it easier to consume SmallRye components instead of WildFly Elytron components. Keep an eye on the discussion there for updates and feel free to add your thoughts there as well.

An Overview of the ACME Protocol

Let’s Encrypt is an example of a certificate authority that implements the ACME protocol. To obtain signed certificates from Let’s Encrypt, or any certificate authority that implements the ACME protocol, an ACME client is needed. An ACME client requests signed certificates by sending JSON messages to the desired certificate authority over HTTPS. The certificate authority issues challenges to the ACME client to prove ownership of the requested domain name(s). The challenges usually require having the ACME client set up resources at specific paths with specific content to prove control of the domain name(s) being requested. If the ACME client is able to complete these challenges successfully, the certificate authority will issue a signed certificate.

Elytron’s ACME Client SPI

Let’s take a look now at Elytron’s AcmeClientSpi. This abstract class contains methods for creating and managing an account with a certificate authority as well as methods for obtaining and revoking certificates from a certificate authority. These methods have already been implemented as specified in RFC 8555.

Creating an implementation of AcmeClientSpi

Notice that there are two abstract methods in AcmeClientSpi, proveIdentifierControl and cleanupAfterChallenge. These are the only two methods that a class that extends AcmeClientSpi is required to implement. This means that to make use of Elytron’s AcmeClientSpi in your web server environment, the main step is to create a class that extends AcmeClientSpi and implements proveIdentifierControl and cleanupAfterChallenge. We’ll see how your proveIdentifierControl and cleanUpAfterChallenge method implementations will get used in the section on obtaining a certificate from a certificate authority.

Implementing proveIdentifierControl

public abstract AcmeChallenge proveIdentifierControl​(AcmeAccount account, java.util.List<AcmeChallenge> challenges) throws AcmeException

This method is used to prove control of the identifier (i.e., domain name) associated with the given list of challenges. In particular, this method should select one challenge from the given list of challenges from the certificate authority and then this method should take the steps necessary to respond to the challenge in order to prove that your ACME client does indeed control the domain name that is being requested. This method should return the challenge that was selected. An AcmeException should be thrown if an error occurs while attempting to prove control of the identifier associated with the challenges or if none of the challenge types are supported.

As an example, take a look at the proveIdentifierControl method implementation in our WildFlyAcmeClient. This class extends AcmeClientSpi and is the ACME client implementation that is used by WildFly. Notice that this proveIdentifierControl implementation selects the HTTP challenge type and it then responds to this challenge by provisioning an HTTP resource under the domain name. In particular, it provisions a new file that contains the key authorization and makes this file available using the DOMAIN_NAME/.well-known/acme-challenge/TOKEN URL. The TOKEN value and the key authorization are obtained using the AcmeChallenge#getToken and AcmeChallenge#getKeyAuthorization methods.

Implementing cleanupAfterChallenge

public abstract void cleanupAfterChallenge​(AcmeAccount account, AcmeChallenge challenge) throws AcmeException

This method is used to undo the actions that were taken to prove control of the identifier associated with the given challenge. In particular, once your ACME client has successfully responded to the given challenge via the proveIdentifierControl method, any resources that were provisioned there can be cleaned up. An AcmeException should be thrown if an error occurs while attempting to undo the actions that were taken to prove control of the identifier associated with the given challenge.

As an example, take a look at the cleanupAfterChallenge method implementation in our WildFlyAcmeClient. Notice that this cleanupAfterChallenge implementation simply deletes the file that was created to prove control of the identifier associated with the challenge.

Using your ACME client implementation

Once you have a created a class that extends AcmeClientSpi and implements the two methods that have been described above, you are ready to start using your ACME client to obtain certificates for your web server:

AcmeClientSpi acmeClient = // Your ACME client implementation should be loaded here

A java.util.ServiceLoader can be used to load your AcmeClientSpi implementation. As an example, take a look at how this is done in WildFly. The corresponding META-INF/services descriptor can be seen here.

Creating a certificate authority account

Before obtaining your first certificate from a certificate authority that implements the ACME protocol, you need to create an account with that certificate authority. This can be done using AcmeClientSpi#createAccount. First, we’re going to create an AcmeAccount instance that contains information about the account we want to create. We’re going to do this using an AcmeAccount.Builder:

AcmeAccount acmeAccount = AcmeAccount.builder()
    .setTermsOfServiceAgreed(true)
    .setServerUrl("https://acme-v02.api.letsencrypt.org/directory")
    .setStagingServerUrl("https://acme-staging-v02.api.letsencrypt.org/directory")
    .setContactUrls(new String[] { "mailto:admin@example.com" })
    .build();

In the example above, we’re creating an AcmeAccount instance to be used with the Let’s Encrypt certificate authority. Notice that we’re indicating that we agree to this certificate authority’s terms of service. The server URL is Let’s Encrypt’s ACME server endpoint URL. The staging server URL that we’ve specified is optional and meant to be used when obtaining certificates in a staging environment for testing purposes. Finally, we’ve also specified a contact URL. This is an optional list of contact URLs that the certificate authority can use to notify you about any issues with your account. The AcmeAccount.build() method will generate an account key for you that will be used by your ACME client when communicating with the certificate authority. The generated account key pair can be obtained using AcmeAccount#getPublicKey and AcmeAccount#getPrivateKey. You can store this key pair in a Java key store. Instead of generating an account key, if you have an existing account key that you want to use, the AcmeAccount.Builder#setKey method can be used.

Now we can use our ACME client’s createAccount method to create an account with the certificate authority using the information from our AcmeAccount instance:

acmeClient.createAccount(acmeAccount, false)

Notice that the second parameter indicates whether or not the staging server URL should be used when communicating with the certificate authority. We are setting this to false since we don’t want to using the staging URL, i.e., going back to our example, we want to use https://acme-v02.api.letsencrypt.org/directory when communicating with Let’s Encrypt.

The createAccount method will return true if the account was created or false if the account already existed. In both cases, acmeAccount#getAccountUrl can then be used to obtain the new or existing account URL that was provided by the certificate authority.

Updating the contact URLs associated with a certificate authority account

To update the contact URLs associated with an existing account, the AcmeClientSpi#updateAccount method can be used:

acmeClient.updateAccount(acmeAccount, false, new String[] { "mailto:admin@example.com", "mailto:anotheradmin@example.com"});

The first parameter is our AcmeAccount instance that contains the information associated with our account. The second parameter indicates whether or not the staging server URL should be used when communicating with the certificate authority. The last parameter includes our updated contact URLs.

Obtaining a certificate from a certificate authority

To obtain a certifcate from a certificate authority, the AcmeClientSpi#obtainCertificate method can be used. This method will prove ownership of the requested domain name(s) using your proveIdentifierControl implementation. This method will also generate a key pair, generate a certificate signing request (CSR) using the generated key pair and the requested domain names, and it will submit this CSR to the certificate authority. If successful, this method will retrieve the resulting certificate chain from the certificate authority. Finally, this method uses your cleanupAfterChallenge implementation to undo any actions that were taken to prove control of your requested domain name(s). The following example shows how to obtain a certificate for www.example.com using our AcmeAccount instance:

X509CertificateChainAndSigningKey certChainAndPrivateKey = acmeClient.obtainCertificateChain(acmeAccount, false, "www.example.com");

The first parameter is our AcmeAccount instance that contains the information associated with our account. The second parameter indicates whether or not the staging server URL should be used when communicating with the certificate authority. The last parameter(s) is the domain name(s) that we want to request a certificate for.

If you want, you can also specify the key algorithm and the key size that the ACME client should use when generating the certificate signing request that will be sent to the certificate authority:

X509CertificateChainAndSigningKey certChainAndPrivateKey = acmeClient.obtainCertificateChain(acmeAccount, false, "EC", 256, "www.example.com");

Notice that the obtainCertificateChain method returns an X509CertificateChainAndSigningKey, which consists of the X.509 certificate chain that was obtained from the certificate authority as well as the private key for your web server that the ACME client generated for you. These can be obtained using the X509CertificateChainAndSigningKey#getCertificateChain method and the X509CertificateChainAndSigningKey#getSigningKey method. You can store this key pair in a Java key store.

Automating certificate renewals

Some certificate authorities, like Let’s Encrypt, recommend renewing your web server’s certificate every 60 days. Renewals can be easily automated by creating a script that checks if your web server’s certificate is due for renewal in the next 30 days and if so, the AcmeClientSpi#obtainCertificateChain method can simply be used to obtain a new certificate. To see an example of how this can be done using WildFly, take a look here.

Revoking a certificate

If you need to revoke a certificate that was issued by a certificate authority, the AcmeClientSpi#revokeCertificate method can be used. For example, to revoke a certificate due to a key compromise, the following can be used:

acmeClient.revokeCertificate(acmeAccount, false, certificateToRevoke, CRLReason.KEY_COMPROMISE);

The first parameter is our AcmeAccount instance that contains the information associated with our account. The second parameter indicates whether or not the staging server URL should be used when communicating with the certificate authority. The third parameter is the X509Certificate that you want to revoke. The fourth parameter is optional and indicates the reason for revoking the certificate. Take a look at CRLReason to see the values that can be specified here.

Updating a certificate authority account key

If you ever need to change the key that is associated with your certificate authority account, the AcmeClientSpi#changeAccountKey method can be used:

acmeClient.changeAccountKey(acmeAccount, false);

The first parameter is our AcmeAccount instance that contains the information associated with our account. The second parameter indicates whether or not the staging server URL should be used when communicating with the certificate authority.

Deactivating an account

If you need to deactivate a certificate authority account, the AcmeClientSpi#deactivateAccount method can be used.

acmeClient.deactivateAccount(acmeAccount, false);

The first parameter is our AcmeAccount instance that contains the information associated with our account. The second parameter indicates whether or not the staging server URL should be used when communicating with the certificate authority.

Summary

In this blog post, we’ve taken a detailed look at Elytron’s ACME client SPI. This SPI has already been integrated in WildFly, making it possible to automatically obtain and manage signed certificates using WildFly’s CLI. This blog post has described how Elytron’s ACME client SPI can also be used in other web server environments.