fix(setup): walk TLS chain for trust-store match — Let's Encrypt cleanup

`_read_agnes_ca_pem()` decides whether the served fullchain.pem needs
trust-bootstrapping in the rendered setup prompt. Pre-fix it only
checked the leaf's *immediate* issuer against `certifi`'s trust store.
For Let's Encrypt that's the intermediate (R13), which `certifi` does
not ship — only roots are in trust stores. So a publicly-trusted LE
chain still tripped the "needs bootstrap" path and the setup prompt
emitted a step-0 TLS trust block + clone-fallback marketplace block
that no client actually needs (Bun-compiled `claude.exe`, system git,
Python via certifi all validate the chain through the bundled ISRG
Root X1).

Now we walk every cert in the fullchain (leaf + intermediates) and
return None the first time any cert's issuer is in the certifi trust
store — that captures the standard "leaf signed by intermediate signed
by publicly-trusted root" shape. Trusted subjects are read once into
a set for O(1) lookup. Self-signed (leaf.issuer == leaf.subject) and
private-CA chains (no chain link's issuer in certifi) keep their
previous "return PEM" behavior, so deployments that genuinely need
the bootstrap still get it.

Validated end-to-end against the live VM at
agnes-marustamyan.groupondev.com (LE R13 → ISRG Root X1):
  - Let's Encrypt fullchain                   → has_ca=False (was True)
  - Self-signed cert                          → has_ca=True
  - Corporate-CA chain (private root)         → has_ca=True
  - Missing fullchain.pem                     → has_ca=False
This commit is contained in:
Minas Arustamyan 2026-05-05 04:55:06 +02:00
parent 9d53efc6e1
commit af72c5d259

View file

@ -163,12 +163,20 @@ def _read_agnes_ca_pem() -> Optional[str]:
"""Read the Agnes server's TLS fullchain for inlining into the setup prompt. """Read the Agnes server's TLS fullchain for inlining into the setup prompt.
Returns the PEM string when the cert needs trust-bootstrapping Returns the PEM string when the cert needs trust-bootstrapping
self-signed (subject == issuer of the leaf), private CA chain, or any self-signed (leaf issuer == subject), private-CA chain that doesn't
case where we can't cheaply prove the OS would trust it. Returns None terminate in a `certifi`-known root, or any case where we can't
only for chains where the leaf's issuer matches a CA already in the cheaply prove the OS would trust it. Returns None when the chain in
server's `certifi`-backed default trust store (publicly-trusted CA the served fullchain.pem terminates in a publicly-trusted root that
like Let's Encrypt or a real corp PKI root that's distributed widely `certifi` already ships (Let's Encrypt's ISRG Root X1, DigiCert,
enough to be in `certifi`). etc.) clients (Bun-compiled `claude.exe`, system git, Python with
certifi) all accept the chain without help.
Chain validation walks every cert in the served fullchain and
succeeds the first time any cert's issuer matches a `certifi` root
subject. That captures the standard fullchain shape (leaf +
intermediate(s)) where `intermediate.issuer == publicly_trusted_root`,
even though the leaf's *immediate* issuer is the intermediate (which
is rarely shipped in trust stores only roots are).
Inlining a publicly-trusted cert is harmless (clients already trust Inlining a publicly-trusted cert is harmless (clients already trust
it via OS roots), but it bloats the prompt and steers users into it via OS roots), but it bloats the prompt and steers users into
@ -193,18 +201,20 @@ def _read_agnes_ca_pem() -> Optional[str]:
try: try:
from cryptography import x509 from cryptography import x509
# Parse just the first cert in the chain — that's the leaf, and chain = x509.load_pem_x509_certificates(pem.encode("utf-8"))
# leaf issuer/subject is what determines self-signed vs CA-signed. if not chain:
first_block = pem.split("-----END CERTIFICATE-----", 1)[0] + "-----END CERTIFICATE-----\n" return None
leaf = x509.load_pem_x509_certificate(first_block.encode("utf-8")) leaf = chain[0]
if leaf.issuer == leaf.subject: if leaf.issuer == leaf.subject:
# Self-signed — definitely needs bootstrap on the client. # Self-signed — definitely needs bootstrap on the client.
return pem return pem
# CA-signed leaf: check whether `certifi`'s trust store already # CA-signed leaf: walk every cert in the served fullchain (leaf +
# contains the issuer. If yes, the user's `da`/uv (which both # intermediates) and check whether ANY of their issuers is in
# use `certifi` by default) will validate without our help. # `certifi`'s trust store. The first match means the chain
# terminates in a publicly-trusted root, so the client OS / Bun
# bundle / certifi already accept it.
try: try:
import certifi import certifi
with open(certifi.where(), "rb") as fh: with open(certifi.where(), "rb") as fh:
@ -212,9 +222,12 @@ def _read_agnes_ca_pem() -> Optional[str]:
except Exception: except Exception:
return pem # can't enumerate trust → assume bootstrap needed return pem # can't enumerate trust → assume bootstrap needed
issuer_dn = leaf.issuer.rfc4514_string() trusted_subjects = {
for ca in x509.load_pem_x509_certificates(trust_pem): ca.subject.rfc4514_string()
if ca.subject.rfc4514_string() == issuer_dn: for ca in x509.load_pem_x509_certificates(trust_pem)
}
for cert in chain:
if cert.issuer.rfc4514_string() in trusted_subjects:
return None # publicly trusted; client OS already accepts return None # publicly trusted; client OS already accepts
return pem return pem
except Exception: # pragma: no cover — defensive: bad PEM / x509 error except Exception: # pragma: no cover — defensive: bad PEM / x509 error