#2583·capnproto

Feature request: support AIA certificate chain completion in kj-tls

Author: dsfacciniCreated Mar 4, 2026Updated Mar 18, 2026

Hey @kentonv, I stumbled upon an issue where I wanted to fetch a PDF from a domain that had this certificate problem, so after much looking for a solution I'm now very certain there's no workaround for this. I asked Claude how hard it would be to support a fix for this and it doesn't seem too extensive. I'd be happy to contribute it but since I'm no C++ expert I rely wholly on Claude to review the code. So to summarize, the question would be: is this something you and the team would be willing to support in workerd? Lmk and also lmk if you want me to contribute the change, if I can spare you some work.

Below is Claude's detailed report on the issue:


Feature request: support AIA (Authority Information Access) certificate chain completion in kj-tls

Problem

When running a Cloudflare Worker locally via wrangler dev (which uses workerd), fetch() to a server that sends an incomplete TLS certificate chain (leaf cert only, missing intermediate) fails with:

workerd/jsg/util.c++:275: error: e = kj/compat/tls.c++:221: failed: TLS peer's certificate is not trusted; reason = unable to get local issuer certificate

exposed to the Worker as:

Error: internal error

Many real-world servers are misconfigured this way. Browsers and curl handle this transparently by reading the AIA extension from the leaf certificate, fetching the missing intermediate from the URL it provides, and completing the chain before verification.

KJ's TLS layer (and BoringSSL underneath) performs strict chain validation with no AIA fetching, so these connections always fail in local workerd.

Note: Deployed Workers on Cloudflare's edge network handle this fine — CF's edge TLS stack is more forgiving. This issue is specific to local workerd, which affects wrangler dev and any standalone workerd usage.

Environment

  • OS: macOS 26.3 (Darwin 25.3.0, arm64)
  • Node: v24.13.0
  • Wrangler: 4.70.0
  • Package manager: pnpm 9.15.0
  • Compatibility date: 2026-03-04
  • Compatibility flags: nodejs_compat_v2

Reproducible example

Minimal worker:

typescript
export default {
  async fetch(request: Request): Promise<Response> {
    try {
      const res = await fetch("https://s3-legispan.asamblea.gob.pa/legispan/NORMAS/2020/2026/LEY/ericka%20lopez_30465_2026_2_13_ASAMBLEA%20NACIONAL_508.pdf");
      return new Response(`Success: ${res.status}, size: ${res.headers.get('content-length')}`, { status: 200 });
    } catch (e: unknown) {
      const msg = e instanceof Error ? e.message : String(e);
      return new Response(`Failed: ${msg}`, { status: 500 });
    }
  },
};

Result with wrangler dev (local workerd):

Failed: internal error

Result in CF Workers Playground (deployed edge):

Success: 200, size: 2970445

incomplete-chain.badssl.com is another stable test server with the same issue — sends only the leaf cert:

bash
$ openssl s_client -connect incomplete-chain.badssl.com:443 2>&1 | grep "Verify return code"
    Verify return code: 21 (unable to verify the first certificate)

The leaf cert's AIA extension points to the missing intermediate:

Authority Information Access:
    CA Issuers - URI:http://r13.i.lencr.org/

Fetching that URL returns the Let's Encrypt R13 intermediate, which completes the chain. Browsers do this automatically; kj-tls does not, so local workerd fails to connect.

Why this matters

This is a blocker for local development with Cloudflare Workers (workerd). When fetch() targets an origin with an incomplete cert chain, there is no workaround — fetch() goes through KJ's TLS layer and there's no option to skip validation or supply intermediates per-request. Developers must either use wrangler dev --remote (which doesn't support all bindings) or avoid these origins entirely.

Community threads reporting this:

A similar feature request exists for OpenSSL itself: openssl/openssl#27016.

Possible approaches

  1. Pre-verification AIA fetch: After receiving the server's cert chain but before calling X509_verify_cert(), extract AIA CA Issuers URLs from the leaf cert, fetch the intermediates via HTTP, and inject them into the X509_STORE. This avoids the sync-callback-in-async-handshake problem since it happens before BoringSSL's verification.

  2. Retry on failure: Let verification fail, then check whether the leaf cert has an AIA extension, fetch the intermediate, add it to the store, and retry verification.

  3. Opt-in via TlsContext::Options: An enableAiaFetching flag (default false) plus a reference to a kj::Network or kj::HttpClient that the TLS context can use for AIA fetches. This keeps the feature opt-in and avoids a circular dependency (the AIA HTTP client could use a separate non-TLS-wrapped network, since AIA URLs are typically plain HTTP).

End-to-end implementation path

This would require changes in two repos:

1. capnproto (this repo) — kj-tls layer

  • c++/src/kj/compat/tls.h: Add an enableAiaFetching bool and a kj::Maybe<kj::Network&> aiaNetwork to TlsContext::Options, so callers can opt in and provide a network for AIA fetches.
  • c++/src/kj/compat/tls.c++: In the TLS handshake path, after receiving the peer's certificate chain:
    1. Extract AIA CA Issuers URLs from the leaf cert via X509_get_ext_d2i(cert, NID_info_access, ...).
    2. If the chain is incomplete (issuer not in trust store) and an AIA URL is present, fetch the intermediate cert over HTTP using the provided aiaNetwork.
    3. Parse the fetched DER/PEM certificate, add it to the X509_STORE, and proceed with verification.
    4. Cache fetched intermediates (keyed by AIA URL) to avoid repeated fetches.
  • c++/src/kj/compat/tls-test.c++: Add a test that connects to a server presenting an incomplete chain, with AIA fetching enabled, and verifies the handshake succeeds.

The main challenge here is that BoringSSL's verification callbacks are synchronous, but the AIA fetch requires async I/O. The cleanest approach is likely the "retry on failure" pattern: attempt verification, and if it fails with "unable to get local issuer certificate", do the AIA fetch asynchronously, add the cert to the store, and retry. This fits naturally into KJ's async model since the TLS handshake is already a kj::Promise.

2. cloudflare/workerd — wire it up

  • src/workerd/server/server.c++ in makeTlsContext(): Set options.enableAiaFetching = true and pass the unwrapped (non-TLS) network as aiaNetwork. This is ~5 lines of code.
  • Bump the capnproto dependency pin in build/deps/gen/deps.MODULE.bazel to include the kj-tls change.

workerd already has the X.509 AIA parsing code in src/workerd/api/crypto/x509.c++ (safeX509InfoAccessPrint, getInfoAccess), though the kj-tls implementation would use BoringSSL APIs directly rather than depending on workerd's JS-facing API.

Considerations

  • Performance: AIA fetches add latency to the handshake. A certificate cache (keyed by AIA URL) would mitigate this for repeated connections.
  • Security: AIA URLs come from the (untrusted) leaf certificate. Fetches should be constrained: timeout, size limit, HTTP/HTTPS only, no redirects to other schemes.
  • Scope: This only needs to handle the CA Issuers AIA method (OID 1.3.6.1.5.5.7.48.2), not OCSP.