Skip to content

Fix #1632: fall back to certifi when the OS trust store is empty (macOS) - #1933

Closed
vinayK34 wants to merge 2 commits into
httpie:masterfrom
vinayK34:fix/1632-certifi-fallback-empty-trust-store
Closed

Fix #1632: fall back to certifi when the OS trust store is empty (macOS)#1933
vinayK34 wants to merge 2 commits into
httpie:masterfrom
vinayK34:fix/1632-certifi-fallback-empty-trust-store

Conversation

@vinayK34

Copy link
Copy Markdown

Fixes #1632

Problem

On the python.org macOS builds, every HTTPS request fails out of the box:

https: error: SSLError: ... [SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed:
unable to get local issuer certificate

…while requests in the same virtualenv works fine, and https --verify "$(python -m certifi)" ... also works. So it's not a missing certifi β€” HTTPie just isn't finding a trust store.

Root cause

HTTPie always installs its own SSLContext (HTTPieHTTPSAdapter._create_ssl_context, needed to support --ssl / --ciphers). Requests deliberately loads no CA bundle when verify is True and a custom context is in play β€” see HTTPAdapter.cert_verify:

Only load the CA certificates if 'verify' is a string indicating the CA bundle to use. Otherwise, if verify is a boolean, we don't load anything since the connection will be using a context with the default certificates already loaded

So loading the defaults is entirely on us, via compat.ensure_default_certs_loaded() (added for #1583). That function relied solely on SSLContext.load_default_certs(), which uses OpenSSL's default verify paths. On the python.org macOS builds those paths point at a location OpenSSL can't use, so load_default_certs() is a no-op and the context is left with an empty trust store β€” hence "unable to get local issuer certificate" for every host. requests doesn't hit this because its own default is certifi (DEFAULT_CA_BUNDLE_PATH), not the OpenSSL default paths.

Fix

If the OS trust store yields no certificates, fall back to requests.adapters.DEFAULT_CA_BUNDLE_PATH (the certifi bundle) β€” i.e. trust exactly what Requests itself trusts by default:

if not ssl_context.get_ca_certs():
    ssl_context.load_default_certs()

if not ssl_context.get_ca_certs() and os.path.exists(DEFAULT_CA_BUNDLE_PATH):
    ssl_context.load_verify_locations(cafile=DEFAULT_CA_BUNDLE_PATH)

Deliberately conservative:

  • Platforms where the OS trust store does work (Linux, Windows, Homebrew Python) are unchanged β€” the fallback never runs.
  • The system store still wins when present; certifi is only a last resort.
  • No new dependency: certifi is already a hard dependency of requests, and DEFAULT_CA_BUNDLE_PATH is what Requests uses itself.
  • A missing bundle is skipped rather than raising, so this can't turn a verification failure into a crash.
  • --verify=no and --verify=<path> paths are untouched.

Verification

Reproduced and verified in a sandbox with httpie 3.2.4 + requests 2.32.3, forcing OpenSSL's default verify paths to an empty location to emulate the macOS build (SSL_CERT_FILE/SSL_CERT_DIR β†’ empty dir), with certifi installed:

before after
requests.get('https://example.com') 200 200
python -m httpie GET https://example.com exit 1, CERTIFICATE_VERIFY_FAILED exit 0, HTTP/1.1 200 OK

Adapter-level check after the fix, with the OS store empty: HTTPieHTTPSAdapter._create_ssl_context(verify=True) β†’ 121 CA certs loaded, verify_mode=CERT_REQUIRED; verify=False β†’ CERT_NONE (unchanged). --verify=no and a normal environment with a working OS store both still return 200 OK.

tests/test_compat.py adds 4 tests, incl. the regression case and edge cases (already-loaded certs are not reloaded; a missing certifi bundle doesn't raise). test_ensure_default_certs_loaded_falls_back_to_certifi fails against the previous implementation (asserts on an empty get_ca_certs()) and passes with this change β€” confirmed by reverting the function and re-running: 1 failed, 3 passed β†’ 4 passed.

flake8 clean on both changed files.

HTTPie always uses its own `SSLContext` (needed for `--ssl`/`--ciphers`),
so Requests intentionally skips loading any CA bundle for `--verify=yes`
and we load the defaults ourselves via `ensure_default_certs_loaded()`.

That relied solely on `SSLContext.load_default_certs()`, which uses
OpenSSL's default verify paths. On the python.org macOS builds those
paths are empty, so the context ended up with an *empty* trust store and
every HTTPS request failed with CERTIFICATE_VERIFY_FAILED β€” even though
`requests` in the same environment worked fine, because it defaults to
the certifi bundle.

Now, if the OS trust store yields no certificates, we fall back to
`requests.adapters.DEFAULT_CA_BUNDLE_PATH` (certifi), matching what
Requests itself trusts by default.
`test_ensure_default_certs_loaded_falls_back_to_certifi` fails against the
previous implementation (empty trust store) and passes with the fix.
@vinayK34 vinayK34 closed this Aug 11, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

HTTPie does not work on macOS unless certificate bundle is explicitly specified, even though requests does

1 participant