YmerNode.ExtraCaCertificates (Ymer Node v0.7.2)

Copy Markdown View Source

The CA certificates an operator adds to the ones the node's platform trusts, named by path/0 and loaded at boot; every TLS connection the node makes trusts them alongside the platform's own.

A node behind a proxy that re-signs TLS needs to trust a certificate its platform does not ship. Which one is the operator's alone to know, so the node carries none of its own: the operator names one PEM file, and this module puts its certificates into the VM-wide CA cache beside the platform's. Each should be a self-signed CA certificate, the one a chain ends at: Mint verifies a chain against the cache only up to a self-signed certificate, so an intermediate named here loads, and is logged, but anchors no chain.

One cache, loaded once

OTP keeps a single CA cache, :public_key.cacerts_get/0, and Mint reads it for every connection that passes no CA option of its own — every script request through YmerNode.Script.Context.request/2, the browser service client's calls, and any other request the node makes through Req or Mint. Loading the cache once, at boot, is what reaches every one of them with no per-caller code. A script that passes its own :cacerts or :cacertfile trusts only what it names: Mint uses the option in place of the cache, never beside it.

A connection made outside Mint never reads the cache, so it does not trust the extra CA certificates. Typst's package downloads are one: Typst fetches a @preview package with its own TLS client, inside its native library, which trusts only the CA certificates compiled into it.

:public_key.cacerts_load/1 replaces the cache, and OTP has no call that adds to it, so the node combines. It loads the platform's CAs the platform's way, :public_key.cacerts_load/0 — a bundle file on Linux, the system keychains on macOS — and then loads those certificates and the extra ones together, through one temporary PEM file it writes, loads and removes. The platform's set is never read from a file path of the node's choosing, so a host-run release on macOS trusts what its keychains hold, as it would without this module.

The load is the application's first child, ahead of every child that can make a request: the cache fills itself lazily with the platform's CAs alone at the first connection that finds it empty, so a request made before the load would trust less than the operator named. With no file named the module does nothing, and the cache loads lazily as it always has.

The file, and when it refuses boot

The file must be a readable regular file of PEM blocks, every block a certificate that decodes, and at least one. Text outside the blocks is allowed — the subject= and Bag Attributes lines an export writes — and so are CRLF line endings. Anything else refuses boot, with one message per cause naming the setting, the path, what was found and its fix, each ending with the way to boot now: remove the setting.

The node classifies the file itself rather than leaving it to OTP, whose own load cannot word a refusal: it answers {:error, :enoent} for a missing file, an empty one, a DER file and a key alike, and it loads the good certificates of a file with a damaged block beside them without a word. A node trusting part of what its operator named, silently, is worse than one that says which block broke. A key among the certificates usually means the wrong file was named, so it refuses too.

An expired certificate, or one that is not a CA, is not refused: the operator chose it, and the handshake judges it as it judges any anchor.

With a file named, the platform's own CAs failing to load refuses boot as well: trusting the extra certificates alone would leave every other host untrusted, and each connection to one would fail far from this cause. A successful load logs one line naming the file, how many certificates it added and each one's subject — what an operator checks in the node's boot log to see that the right file was read.

Summary

Functions

Returns a specification to start this module under a supervisor.

Loads the platform's CAs and the extra CA certificates in path into the CA cache as one set, and logs one line naming what it added.

The PEM file of extra CA certificates (config :ymer_node, YmerNode.ExtraCaCertificates, :path), returned as configured: an absolute path, which the configuration that runs the node expands, and nil when none is named.

Reads the extra CA certificates in path: a readable regular file of PEM blocks, every block a certificate that decodes, and at least one.

The subject of the certificate der: each attribute as <name>=<value>, in the certificate's own order, joined by , — the form openssl x509 -noout -subject prints for a subject of single-valued attributes. Values are written as they are, never escaped; the attributes of a multi-valued RDN are joined by +, an attribute with no short name is written by its dotted OID, and a value OTP does not decode is written as # and the hex of its DER, RFC 4514's form.

Functions

child_spec(init_arg)

Returns a specification to start this module under a supervisor.

See Supervisor.

install!(path, load_platform \\ &:public_key.cacerts_load/0)

Loads the platform's CAs and the extra CA certificates in path into the CA cache as one set, and logs one line naming what it added.

Raises ArgumentError, carrying the message an operator reads in the boot log, when the file breaks the rule read/1 checks — before the platform's CAs are touched — or when the platform's own CAs do not load. load_platform is that load, :public_key.cacerts_load/0; it is an argument only so a test can stand in a platform that fails.

path()

The PEM file of extra CA certificates (config :ymer_node, YmerNode.ExtraCaCertificates, :path), returned as configured: an absolute path, which the configuration that runs the node expands, and nil when none is named.

read(path)

Reads the extra CA certificates in path: a readable regular file of PEM blocks, every block a certificate that decodes, and at least one.

Answers each certificate's DER in file order, or {:error, message} with the refusal an operator reads — one message per cause, naming the variable, the path, what was found and its fix.

start_link(opts)

subject(der)

The subject of the certificate der: each attribute as <name>=<value>, in the certificate's own order, joined by , — the form openssl x509 -noout -subject prints for a subject of single-valued attributes. Values are written as they are, never escaped; the attributes of a multi-valued RDN are joined by +, an attribute with no short name is written by its dotted OID, and a value OTP does not decode is written as # and the hex of its DER, RFC 4514's form.