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.