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
- Elytron’s ACME Client SPI
- Creating an implementation of AcmeClientSpi
- Using your ACME client implementation
- Creating a certificate authority account
- Updating the contact URLs associated with a certificate authority account
- Obtaining a certificate from a certificate authority
- Automating certificate renewals
- Revoking a certificate
- Updating a certificate authority account key
- Deactivating an account
- Summary
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.