How to check if certificate needs renewal without ordering a new one?
cowwoc opened this issue · comments
When my server starts up it might already have a valid certificate. The only way I know of checking if renewal is needed is using RenewalInfo
but the only way to get an instance is by having a Certificate
and the only way I know of getting one is by ordering a new one.
I am sure I am overlooking something in the API. I read through the documentation and couldn't find a discussion of this use-case.
What am I supposed to do this in case?
I think I get it now. If I create an order asking for a new Certificate and the status of the request comes back with VALID
then it means that a certificate already exists and does not need to be renewed. Is that correct?
I think I get it now. If I create an order asking for a new Certificate and the status of the request comes back with
VALID
then it means that a certificate already exists and does not need to be renewed. Is that correct?
No. If the certificate has the status VALID
, it only means that the certificate has been created and is in a downloadable state. It does not reflect whether the certificate has expired or not. You also wouldn't want to wait with renewal until the cert has already been expired.
This is a way to get the RenewalInfo
object: If you got your certificate, you can use the Certificate.getLocation()
method to retrieve the URL of the certificate on CA side. You can store this URL somewhere, e.g. in a database. To recreate the Certificate
object at a later stage, you can invoke Login.bindCertificate(certificateUrl)
. Then you can invoke Certificate.getRenewalInfo()
and get the RenewalInfo
object.
Example:
Certificate cert = // the certificate that was freshly created
URL certLocation = cert.getLocation();
// store certLocation somewhere
Then, as soon as you need the RenewalInfo
:
Login login = // your login
URL certLocation = // certLocation that was stored
Certificate cert = login.bindCertificate(certLocation);
RenewalInfo renewalInfo = cert.getRenewalInfo();
Anyhow I just realized that this way is unnecessary complicated. I will think about an improved way, which directly uses the X509Certificate
object. I will keep this issue open, and report back as soon as it is available.
Note that RenewalInfo
must be supported by the CA, otherwise getRenewalInfo()
will throw an exception. To generally find out if a certificate will expire soon, you can also use the X509Certificate.getNotAfter()
method to read the certificate's expiry date.
A better way (which still requires storing an URL though):
Certificate cert = // the certificate that was freshly created
Optional<URL> renewalInfoLocation = cert.getRenewalInfoLocation();
// Store the renewalInfoLocation somewhere. Will be empty if renewalInfo is not supported.
Then later:
Login login = // your login
URL renewalInfoLocation = // renewalInfoLocation that was stored
RenewalInfo renewalInfo = login.bindRenewalInfo(renewalInfoLocation);
The "improved way" which I mentioned above will not require to store an URL, but it will require draft-ietf-acme-ari-02
, which is not supported yet.
My incentive for asking this question is that Let's Encrypt has rate limits of issuing 5 certificates per week (for the same domains). I don't want to cross this limit, but even time I deploy a new server to production it restarts acme4j and goes through the certificate renewal process all over again.
At what step does acme4j ask for a new certificate? Is it Order.execute(Keypair domainKeyPair)
? Can I run the following code indefinitely without triggering their limit?
// Order the certificate
Order order = account.newOrder().domains(domains).create();
// Perform all required authorizations
for (Authorization auth : order.getAuthorizations())
verify(auth);
if (order.getStatus() == Status.VALID)
{
Certificate certificate = order.getCertificate();
X509Certificate x509Certificate = certificate.getCertificate();
Date endTime = x509Certificate.getNotAfter();
Instant now = Instant.now();
Duration timeLeft = Duration.between(now, endTime.toInstant());
if (timeLeft.compareTo(MIN_TIME_LEFT) > 0)
{
// No need to renew certificate
return certificate;
}
}
// Otherwise, renew certificate
This way I wouldn't need to store the certificate anywhere. I would just download it on demand.
Can you please update the example code and documentation (Javadoc and main doc) to tackle this use-case. Also, it would be helpful if you indicates that getRenewalInfo()
may not be supported by the server. As it stands, the @return
Javadoc indicates that a value is always returned.
Thank you.
With account.newOrder()
you won't retrieve an existing order/certificate, but prepare to creating a new one.
The ACME protocol provides a way to fetch all existing orders that are related to your account, and acme4j offers access to this information with Account.getOrders()
. But although this field is mandatory according to RFC 8555, it is not implemented at Let's Encrypt. See letsencrypt/boulder#3335 and #74.
So if you use Let's Encrypt, I see no other way than to either store the order or certificate URLs locally, or check your locally stored certificate for expiration. However, checking x509Certificate.getNotAfter()
is the correct way to find out if your certificate needs to be renewed soon.
I will review the RenewalInfo
references in the Javadocs and documentation, and mention that it needs to be supported by the CA.
@shred What other services is acme4j compatible with? Does it support ZeroSSL?
acme4j is designed to be a generic ACME client in first place, so it is compatible with all CAs that are RFC 8555 compliant. It is best tested with Let's Encrypt though.
ZeroSSL should work. You can connect to their servers by using the https://acme.zerossl.com/v2/DV90
URI (instead of acme://letsencrypt.org
).
Thank you. I will close this issue, seeing as ZeroSSL doesn't have rate limits. I will just issue a new certificate on every startup for now, and eventually I'll add state to the database to avoid unnecessary renews.