oven-sh/bun · error · CertError

INVALID_CA

INVALID_CA

Error message

INVALID_CA

What it means

CertError::INVALID_CA maps BoringSSL X509 verify code 24 (X509_V_ERR_INVALID_CA) via get_cert_error_from_no() in src/http/lib.rs:1543. A certificate used as a CA in the path is not actually a CA: its Basic Constraints lack CA:TRUE, or its key type/usage disallows acting as one. JS error.code is INVALID_CA with message 'invalid CA certificate' (FetchTasklet.rs:1440).

Source

Thrown at src/http/error.rs:169

    #[error("ERROR_IN_CRL_LAST_UPDATE_FIELD")]
    ERROR_IN_CRL_LAST_UPDATE_FIELD,
    #[error("ERROR_IN_CRL_NEXT_UPDATE_FIELD")]
    ERROR_IN_CRL_NEXT_UPDATE_FIELD,
    #[error("OUT_OF_MEM")]
    OUT_OF_MEM,
    #[error("DEPTH_ZERO_SELF_SIGNED_CERT")]
    DEPTH_ZERO_SELF_SIGNED_CERT,
    #[error("SELF_SIGNED_CERT_IN_CHAIN")]
    SELF_SIGNED_CERT_IN_CHAIN,
    #[error("UNABLE_TO_GET_ISSUER_CERT_LOCALLY")]
    UNABLE_TO_GET_ISSUER_CERT_LOCALLY,
    #[error("UNABLE_TO_VERIFY_LEAF_SIGNATURE")]
    UNABLE_TO_VERIFY_LEAF_SIGNATURE,
    #[error("CERT_CHAIN_TOO_LONG")]
    CERT_CHAIN_TOO_LONG,
    #[error("CERT_REVOKED")]
    CERT_REVOKED,
    #[error("INVALID_CA")]
    INVALID_CA,
    #[error("PATH_LENGTH_EXCEEDED")]
    PATH_LENGTH_EXCEEDED,
    #[error("INVALID_PURPOSE")]
    INVALID_PURPOSE,
    #[error("CERT_UNTRUSTED")]
    CERT_UNTRUSTED,
    #[error("CERT_REJECTED")]
    CERT_REJECTED,
    #[error("SUBJECT_ISSUER_MISMATCH")]
    SUBJECT_ISSUER_MISMATCH,
    #[error("AKID_SKID_MISMATCH")]
    AKID_SKID_MISMATCH,
    #[error("AKID_ISSUER_SERIAL_MISMATCH")]
    AKID_ISSUER_SERIAL_MISMATCH,
    #[error("KEYUSAGE_NO_CERTSIGN")]
    KEYUSAGE_NO_CERTSIGN,
    #[error("UNABLE_TO_GET_CRL_ISSUER")]

View on GitHub (pinned to 8c5296ac45)

Solutions

  1. Regenerate the CA with explicit CA:TRUE (see exampleFix) and reissue the leaf from it
  2. Check the intermediate: openssl x509 -in int.pem -text -noout | grep -A2 'Basic Constraints' must show CA:TRUE
  3. Serve/anchor the correct intermediate that actually has CA:TRUE
  4. Pass the real CA (the one that signed the leaf) in tls: { ca }, not the leaf itself

Example fix

# before: leaf-style self-signed cert used as CA
openssl req -x509 -newkey rsa:2048 -keyout k.pem -out c.pem -days 30
# after: proper CA with Basic Constraints
openssl req -x509 -newkey rsa:2048 -keyout ca-key.pem -out ca.pem -days 3650 \
  -addext 'basicConstraints=critical,CA:TRUE' \
  -addext 'keyUsage=critical,keyCertSign,cRLSign'
Defensive patterns

Strategy: validation

Validate before calling

import { X509Certificate } from "node:crypto";
const ca = new X509Certificate(await Bun.file("ca.pem").text());
if (!ca.ca) throw new Error("ca.pem lacks BasicConstraints CA:TRUE — regenerate it before using as tls.ca");
await fetch(url, { tls: { ca: ca.raw.toString() } });

Type guard

function isInvalidCa(e: unknown): e is Error & { code: "INVALID_CA" } {
  return e instanceof Error && (e as any).code === "INVALID_CA";
}

Try / catch

try { await fetch(url, { tls: { ca } }); } catch (e) { if (isInvalidCa(e)) { failFast("trust anchor is not a real CA — fix ca.pem generation"); } throw e; }

Prevention

When it happens

Trigger: A self-signed leaf minted with openssl req -x509 WITHOUT basicConstraints=CA:TRUE is passed via tls: { ca } or installed as a root; an intermediate in the served chain lacks CA:TRUE; a server cert being used to sign another cert.

Common situations: Dev/test self-signed certs generated with defaults used as trust anchors (very common in docker-compose local stacks), mkcert-less local CA attempts, private PKI where the intermediate template omitted Basic Constraints.

Related errors


AI-assisted analysis of oven-sh/bun@8c5296ac45 (2026-08-16). Data as JSON: /api/errors/d0153c649d9b745e. Report an issue: GitHub.