shred / acme4j

Java client for ACME (Let's Encrypt)

Home Page:https://acme4j.shredzone.org

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

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.