quarkusio/quarkus · error · IllegalArgumentException

The certificate chain cannot be null or empty

Error message

The certificate chain cannot be null or empty

What it means

writePrivateKeyAndCertificateChainsAsPem also validates the X509Certificate[] argument and throws IllegalArgumentException when it is null or zero-length. A certificate chain is mandatory to write the chain PEM; an empty result means the ACME order returned no certificates, so there is nothing usable to persist.

Source

Thrown at extensions/tls-registry/cli/src/main/java/io/quarkus/tls/cli/letsencrypt/LetsEncryptHelpers.java:46

import io.smallrye.certs.CertificateUtils;
import io.vertx.core.json.JsonObject;

public class LetsEncryptHelpers {

    public static final String DEFAULT_ACME_URL = "https://acme-v02.api.letsencrypt.org/directory";
    public static final String DEFAULT_ACME_STAGING_URL = "https://acme-staging-v02.api.letsencrypt.org/directory";
    public static final String TLS_AUDIT_LOG = "io.quarkus.tls.audit";

    static Logger LOGGER = Logger.getLogger(LetsEncryptHelpers.class);
    public static Logger AUDIT = Logger.getLogger(LetsEncryptHelpers.TLS_AUDIT_LOG);

    public static void writePrivateKeyAndCertificateChainsAsPem(PrivateKey pk, X509Certificate[] chain, File privateKeyFile,
            File certificateChainFile) throws Exception {
        if (pk == null) {
            throw new IllegalArgumentException("The private key cannot be null");
        }
        if (chain == null || chain.length == 0) {
            throw new IllegalArgumentException("The certificate chain cannot be null or empty");
        }

        AUDIT.debug("Writing private key to file: " + privateKeyFile.getAbsolutePath());
        CertificateUtils.writePrivateKeyToPem(pk, null, privateKeyFile);

        AUDIT.debug("Writing certificate chain to file: " + certificateChainFile.getAbsolutePath());

        if (chain.length == 1) {
            CertificateUtils.writeCertificateToPEM(chain[0], certificateChainFile);
            return;
        }

        // For some reason the method from CertificateUtils distinguishes the first certificate and the rest of the chain
        X509Certificate[] restOfTheChain = new X509Certificate[chain.length - 1];
        System.arraycopy(chain, 1, restOfTheChain, 0, chain.length - 1);
        CertificateUtils.writeCertificateToPEM(chain[0], certificateChainFile, restOfTheChain);
    }

View on GitHub (pinned to e1c734241f)

Solutions

  1. Null/empty-check getCertificateChain() before writing; if empty, treat the order as failed and re-run lets-encrypt issue/renew.
  2. Inspect the ACME order state (pending/invalid/valid) — wait for pending orders or fix authorization failures (DNS/HTTP-01 reachability) before retrying.
  3. Test with the Let's Encrypt staging URL to rule out a custom-server incompatibility.
  4. Ensure the domain in the CLI command resolves and is reachable over HTTP 80 so the HTTP-01 challenge can succeed and a chain is actually issued.

Example fix

// before
X509Certificate[] chain = certChainAndPrivateKey.getCertificateChain();
LetsEncryptHelpers.writePrivateKeyAndCertificateChainsAsPem(key, chain, keyPem, chainPem);

// after
X509Certificate[] chain = certChainAndPrivateKey.getCertificateChain();
if (chain == null || chain.length == 0) {
    throw new IllegalStateException("No certificate chain returned by ACME; order likely failed");
}
LetsEncryptHelpers.writePrivateKeyAndCertificateChainsAsPem(key, chain, keyPem, chainPem);
Defensive patterns

Strategy: validation

Validate before calling

X509Certificate[] chain = certChainAndPrivateKey.getCertificateChain();
if (chain == null || chain.length == 0) {
    throw new IllegalStateException("ACME returned no certificate chain; order failed or pending");
}
LetsEncryptHelpers.writePrivateKeyAndCertificateChainsAsPem(pk, chain, keyPem, chainPem);

Type guard

static boolean hasCertificateChain(X509CertificateChainAndSigningKey r) {
    return r != null && r.getCertificateChain() != null && r.getCertificateChain().length > 0;
}

Try / catch

try {
    LetsEncryptHelpers.writePrivateKeyAndCertificateChainsAsPem(pk, chain, keyPem, chainPem);
} catch (IllegalArgumentException e) {
    if (e.getMessage() != null && e.getMessage().contains("certificate chain cannot be null")) {
        // no chain issued: check ACME order state / authorization, then retry issuance
    } else {
        throw e;
    }
}

Prevention

When it happens

Trigger: Calling writePrivateKeyAndCertificateChainsAsPem(pk, null, ...) or with new X509Certificate[0], or via issueCertificate when obtainCertificateChain's X509CertificateChainAndSigningKey.getCertificateChain() is null/empty (failed ACME authorization, order still pending/invalid, custom server bug).

Common situations: Renewing while the previous order is still pending at the CA; domain authorization failed so the CA returned no chain; tests/tools calling the helper with placeholder data; a custom ACME directory that returns a malformed finalize response.

Understand the failure class

Related errors


AI-assisted analysis of quarkusio/quarkus@e1c734241f (2026-09-05). Data as JSON: /api/errors/b1182604c144ad77. Report an issue: GitHub.