CA-certificate loading for mob apps. Companion to Mob.DNS — same
shape: a small wrapper documenting and working around something OTP
assumes about the OS that Android doesn't satisfy.
Why this exists
:public_key.cacerts_load/0 looks for a system CA bundle at one of
the distro paths it knows (/etc/ssl/certs/ca-certificates.crt,
/etc/pki/tls/certs/ca-bundle.crt, /etc/ssl/cert.pem, …). On
Android none of those exist — the system trust store lives behind a
Java API that BEAM's :public_key doesn't reach. Subsequent calls
to :public_key.cacerts_get/0 therefore raise with no_cacerts_found,
and any library that consults it (Req → Mint → :ssl, Finch, anything
using OTP-26+ default :ssl opts) crashes on the first TLS connect.
Adding insult: in some OTP versions pubkey_os_cacerts.conv_error_reason/1
has no clause for no_cacerts_found, so the surface error is a
FunctionClauseError — opaque to the unsuspecting reader. The fix is
the same regardless: load a PEM bundle into :public_key once at boot.
Hex itself bakes its own DER bundle, so the BEAM can mix.install/2
without this fix; every other Elixir HTTP library can't.
What to do
Bundle a CA PEM in your app priv (e.g. copy castore's cacerts.pem)
and call Mob.Certs.load_cacerts!/1 once at boot, before anything
tries TLS:
def on_start do
Mob.Certs.load_cacerts!(Application.app_dir(:my_app, "priv/cacerts.pem"))
# …rest of startup…
endThe bundle is the app's choice — security: who do you trust. The
conventional source is the castore hex package (a current Mozilla
trust store), copied into priv/ at build time.
iOS isn't affected — Darwin exposes the trust store via the paths
Erlang knows about, so :public_key.cacerts_load/0 (no arg) works
there. Calling load_cacerts!/1 on iOS at the bundled-PEM path is a
harmless extra load; cross-platform apps can call it unconditionally.
Scope
- Loads CA certificates from a PEM file path.
- Wraps
:public_key.cacerts_load/1so failure shapes are predictable ({:error, reason}rather than the OTP-version-dependentFunctionClauseErroryou sometimes see otherwise). - Pure Elixir. No NIF, no platform branch.
Summary
Functions
Load CA certs from a PEM file into Erlang's :public_key cacert store.
Same as load_cacerts/1, but raises on failure.
True if any CA certificates are loaded in the :public_key store.
Functions
Load CA certs from a PEM file into Erlang's :public_key cacert store.
Idempotent: re-loading the same bundle just re-merges its certs into the in-process trust store; no duplication, no error.
Returns :ok on success or {:error, reason} if the file can't be
read or parsed.
iex> Mob.Certs.load_cacerts("priv/cacerts.pem")
:ok
@spec load_cacerts!(Path.t()) :: :ok
Same as load_cacerts/1, but raises on failure.
Use this at boot when failing-to-load is unrecoverable — i.e. when the app needs HTTPS at all to function. Most callers want this variant.
@spec loaded?() :: boolean()
True if any CA certificates are loaded in the :public_key store.
Useful for diagnostics and tests. :public_key.cacerts_get/0 raises
when nothing is loaded; loaded?/0 catches that and returns false
instead.