Feature request: support AIA certificate chain completion in kj-tls
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 certificateexposed to the Worker as:
Error: internal errorMany 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:
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 errorResult in CF Workers Playground (deployed edge):
Success: 200, size: 2970445incomplete-chain.badssl.com is another stable test server with the same issue — sends only the leaf cert:
$ 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:
- Worker: Is it possible to fetch data from a server that requires an intermediate certificate download?
- How ignore 'cert issues' in Workers subrequests?
A similar feature request exists for OpenSSL itself: openssl/openssl#27016.
Possible approaches
Pre-verification AIA fetch: After receiving the server's cert chain but before calling
X509_verify_cert(), extract AIACA IssuersURLs from the leaf cert, fetch the intermediates via HTTP, and inject them into theX509_STORE. This avoids the sync-callback-in-async-handshake problem since it happens before BoringSSL's verification.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.
Opt-in via
TlsContext::Options: AnenableAiaFetchingflag (default false) plus a reference to akj::Networkorkj::HttpClientthat 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 anenableAiaFetchingbool and akj::Maybe<kj::Network&> aiaNetworktoTlsContext::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:- Extract AIA
CA IssuersURLs from the leaf cert viaX509_get_ext_d2i(cert, NID_info_access, ...). - 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. - Parse the fetched DER/PEM certificate, add it to the
X509_STORE, and proceed with verification. - Cache fetched intermediates (keyed by AIA URL) to avoid repeated fetches.
- Extract AIA
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++inmakeTlsContext(): Setoptions.enableAiaFetching = trueand pass the unwrapped (non-TLS) network asaiaNetwork. This is ~5 lines of code.- Bump the capnproto dependency pin in
build/deps/gen/deps.MODULE.bazelto 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 IssuersAIA method (OID 1.3.6.1.5.5.7.48.2), not OCSP.
Source: capnproto/capnproto