oven-sh/bun · error · CertError

UNABLE_TO_VERIFY_LEAF_SIGNATURE

UNABLE_TO_VERIFY_LEAF_SIGNATURE

Error message

UNABLE_TO_VERIFY_LEAF_SIGNATURE

What it means

CertError::UNABLE_TO_VERIFY_LEAF_SIGNATURE maps BoringSSL X509 verify code 21 (X509_V_ERR_UNABLE_TO_VERIFY_LEAF_SIGNATURE) via get_cert_error_from_no() in src/http/lib.rs:1540. It means the TLS handshake reached certificate verification and the signature on the first (leaf) certificate could not be verified against the issuer candidate the verifier picked. JS code exposes the tag as error.code with message 'unable to verify the first certificate' (FetchTasklet.rs:1431).

Source

Thrown at src/http/error.rs:163

    #[error("CRL_HAS_EXPIRED")]
    CRL_HAS_EXPIRED,
    #[error("ERROR_IN_CERT_NOT_BEFORE_FIELD")]
    ERROR_IN_CERT_NOT_BEFORE_FIELD,
    #[error("ERROR_IN_CERT_NOT_AFTER_FIELD")]
    ERROR_IN_CERT_NOT_AFTER_FIELD,
    #[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")]

View on GitHub (pinned to 8c5296ac45)

Solutions

  1. Run openssl s_client -connect host:443 -showcerts and confirm the leaf's issuer DN matches the subject DN AND public key of the next cert served
  2. Fix the server's fullchain file (leaf first, then the correct intermediate) — usually certbot --nginx or ssl_certificate pointing at fullchain.pem instead of cert.pem
  3. If the chain is intentionally private, pass the matching intermediate: fetch(url, { tls: { ca: intermediatePem } })
  4. Check for re-keyed intermediates: verify the leaf's Authority Key Identifier equals the intermediate's Subject Key Identifier (openssl x509 -text)

Example fix

// before: server serves leaf only
fetch("https://internal.example.com/v1");
// after: client supplies the matching intermediate
const intermediate = await Bun.file("intermediate.pem").text();
await fetch("https://internal.example.com/v1", {
  tls: { ca: intermediate },
});
Defensive patterns

Strategy: try-catch

Validate before calling

import tls from "node:tls";
async function chainVerifies(host: string, port = 443): Promise<boolean> {
  return new Promise((resolve) => {
    const s = tls.connect({ host, port, servername: host }, () => {
      resolve(s.authorized);
      s.destroy();
    });
    s.on("error", () => resolve(false));
  });
}
if (!(await chainVerifies("internal.example.com"))) throw new Error("TLS chain invalid; run openssl s_client -showcerts");

Type guard

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

Try / catch

try {
  const res = await fetch(url);
} catch (e) {
  if (isLeafSignatureError(e)) {
    logger.warn("incomplete/mismatched chain on " + url, { hint: "serve full chain or pass tls.ca" });
    return fallbackToKnownGoodEndpoint();
  }
  throw e;
}

Prevention

When it happens

Trigger: fetch()/Bun.connect with TLS to a server whose served chain is incomplete or misordered (leaf's real intermediate missing, so the verifier matches a different same-named issuer); issuer located but with a different public key; leaf signed with an algorithm BoringSSL will not verify.

Common situations: Server serves only the leaf cert (missing intermediate via certbot/nginx misconfig), chain file contains intermediates in reverse order, cert re-keyed by the CA but old intermediate still served, private PKI issuing from a re-keyed intermediate.

Related errors


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