Skip to content

Why the certificates look the way they do

localca follows mkcert closely — same trust-store mechanics, same per-machine root model — but it is not a copy. Where it differs, it differs on purpose. This page gives the reasoning for each choice, so that changing one later is an informed decision rather than a guess.

Why ECDSA rather than RSA

mkcert defaults to RSA — 3072-bit roots, 2048-bit leaves — and offers ECDSA behind a flag. localca inverts that: ECDSA P-256 always, with no RSA option.

The reason mkcert defaults to RSA is the long tail. It is a general-purpose tool, and somewhere out there is a client that will not negotiate an ECDSA certificate — an old embedded device, an ancient JVM, a legacy library. Defaulting to RSA means mkcert works for everyone, at the cost of larger keys and slower handshakes for the overwhelming majority who did not need it.

localca does not have that long tail. Its consumers are browsers hitting a local web UI and Go clients hitting a loopback listener, and every one of them has supported ECDSA P-256 for a decade. Choosing ECDSA gives smaller certificates, faster key generation — which matters when a leaf is re-minted every 90 days — and faster handshakes, with no realistic compatibility cost.

The cost is real but narrow: if you ever need to serve something that genuinely cannot do ECDSA, localca has no answer. That is the trade being made, and adding a key-algorithm option is the obvious fix if it ever bites.

Why leaves are only valid for 90 days

mkcert issues long-lived leaves — a little over two years — and has no renewal machinery, because a command-line tool that mints a file cannot come back later and mint it again.

localca is a library called at server start-up, so it can. Each EnsureServed checks the cached leaf and re-mints it when it is within seven days of expiring or no longer covers the requested hosts. That makes a short lifetime free: nobody has to remember to renew, and a certificate that leaked from a laptop stops being useful in weeks rather than years.

Ninety days also keeps localca far below the leaf-validity caps that browsers enforce and keep tightening. A tool that issues two-year leaves has to track that ceiling; a tool that issues 90-day leaves never thinks about it again.

Why the leaf sets only DigitalSignature

mkcert sets KeyEncipherment alongside DigitalSignature on its leaves. That is correct for RSA, where the key can be used for key transport — the client encrypts a secret to the server's public key.

An ECDSA key cannot do key transport at all. In an ECDHE-ECDSA handshake the certificate key is used for exactly one thing: signing the ephemeral key exchange. KeyEncipherment on an ECDSA certificate is meaningless — harmless in practice, but a claim about the key that is not true. localca sets DigitalSignature alone because that is what the key actually does.

Why the root cannot sign another CA

The root is minted with MaxPathLenZero, which pins the maximum path length below it to zero: it may sign end-entity certificates and nothing else. A certificate chain that tries to put an intermediate CA under it is invalid.

This costs nothing — localca has no use for intermediates — and it narrows what the key can do if it is ever misused. A root that cannot delegate cannot be used to mint a sub-authority for someone else to sign with.

Why the key is a file and not a keychain entry

The obvious place for a private key on macOS is the login keychain, and on Linux the Secret Service. localca stores it as a 0600 file under the data directory instead.

Two reasons. The first is practical: keychain access blocks on a desktop session, and localca is expected to run on headless servers, in containers and in CI, where that session does not exist and the call hangs rather than failing. The second is architectural: localca is a framework-free module with a deliberately tiny dependency graph, enforced by a test that fails the build if a forbidden dependency appears. A keychain binding is a platform-specific dependency for every consumer, in exchange for protecting a key that is already scoped to one machine and one user account.

The file permissions are the protection, and the honest statement of the trade-off is that they are all of it. Anyone who can read files as your user can read the root key. So can anything that runs as you.

Why the trust-store install is someone else's code

Installing a root into an OS trust store is a different mechanism on every platform, and several mechanisms on some of them: anchor directories that differ per Linux distribution along with the update command that goes with each, a macOS keychain trust-settings plist that has to be exported, patched and re-imported, a Windows CryptoAPI call, and NSS databases in two on-disk formats scattered across browser profiles. Getting it wrong means either a silent no-op or a corrupted Firefox profile.

localca uses github.com/smallstep/truststore, which is mkcert's trust-store code extracted into a library — the platform files still carry the mkcert copyright. That means the install path is mkcert's, by construction rather than by re-implementation, and improvements to it arrive as a dependency bump.

What that buys is proven behaviour. What it costs is the ability to change how the install happens: the library hard-codes sudo, which is exactly why there is no GUI password dialog.

Why system-store trust is remembered rather than queried

localca records its own system-store installs in a small JSON marker under the data directory and treats that as the answer to "is the root trusted?". Reading a fact you wrote yourself is obviously weaker than asking the operating system.

There is no portable way to ask the operating system. Each platform has a different answer and some have none at all that does not require elevation. Meanwhile the question localca actually needs answered is narrower than it looks: did we already install this root, so can we skip the password prompt? The marker answers that exactly, keyed by the root's fingerprint so a re-minted root is never mistaken for the old one.

The cost is that a root removed by other means goes undetected — the caveat mkcert has too, with the same remedy of installing again.

Why NSS is never allowed to fail the operation

Firefox does not read the OS trust store. It keeps its own NSS database, and writing to it needs certutil from a package that is frequently not installed — never on a minimal container, often not on a headless server.

If NSS were required, EnsureServed would fail on every machine without certutil, for the sake of a browser that may not even be installed there. So the system store alone decides whether provisioning succeeded, and NSS is attempted alongside it and allowed to fail silently. Chrome, Safari, Edge and Go's HTTP client all read the system store; Firefox trust is a bonus that most machines get and some do not.

This is also mkcert's posture, and the reason TrustState reports the two stores separately rather than as one boolean.