Skip to content

Spike: mkcert parity & the elevation question

Date
16 July 2026
Status
INFORMATIONAL — findings feed the go/localca design (spec 2026-07-16-localca-module.md).

TL;DR

  • Our install path is mkcert's. go/localca wraps github.com/smallstep/truststore, which is mkcert's trust-store code extracted verbatim into a library — every platform file carries Copyright (c) 2018 The mkcert Authors. So we have mkcert's cross-OS install behaviour by construction, not by re-implementation.
  • Functional parity is high. We match root/leaf shape, validity model, SAN handling for our use case, and system + NSS stores. Deliberate differences: ECDSA by default (mkcert defaults to RSA), 90-day auto-renewing leaves (mkcert issues long-lived ones), and we skip Java + email/URI SANs (not needed by keryx/krites).
  • The elevation gap is the real finding. mkcert/truststore elevate with sudo on both Linux and macOS — a terminal password prompt, not a desktop dialog. That is fine for keryx's terminal-launched studio, but fails for a GUI app with no terminal (krites .dmg). A desktop dialog needs OS-native elevation (osascript on macOS, pkexec on Linux), which truststore does not provide.

1. How mkcert installs a local CA (via truststore)

System store (needs elevation)

OS Mechanism (exact commands) Elevation
Linux sudo -- tee <anchor>/<name>.crt (writes the PEM) then sudo -- update-ca-certificates (or update-ca-trust extract / trust extract-compat by distro) sudo (terminal)
macOS sudo security add-trusted-cert -d -k /Library/Keychains/System.keychain <cert>, then trust-settings-export → patch the plist → trust-settings-import sudo (terminal)
Windows CryptoAPI root store via certutil UAC

The anchor dir is auto-detected (/usr/local/share/ca-certificates, /etc/pki/ca-trust/..., SUSE, Arch, /etc/ssl/certs). CommandWithSudo prepends sudo -- only if sudo is on PATH; otherwise it runs the command bare (assumes already-root).

NSS store — Firefox & Chromium (no elevation)

User-level via certutil against every NSS DB: Firefox profiles + ~/.pki/nssdb (which Chrome/Chromium read on Linux). certutil -A -d <profile> -t "C,," -n <name> -i <cert>. Needs libnss3-tools; absent ⇒ mkcert prints a help line and skips NSS. This is the path we can prove on this headless box (user-level, into ~/.pki/nssdb).

2. Point-by-point: mkcert vs go/localca

Aspect mkcert (gold standard) go/localca Verdict
Root key RSA 3072 default (--ecdsa opt-in P-256) ECDSA P-256 Deliberate — see §3
Root validity 10 years ~10 years ✅ match
Root constraints IsCA · MaxPathLenZero · BasicConstraintsValid same ✅ match
Leaf key RSA 2048 default (ECDSA opt-in) ECDSA P-256 Deliberate — §3
Leaf validity ~2y3m (kept < 825d for macOS/iOS) 90 days, auto-renewed ✅ safer; §3
Leaf KeyUsage KeyEncipherment \| DigitalSignature (RSA) DigitalSignature (ECDSA-correct) ✅ fix applied
Leaf ExtKeyUsage ServerAuth (+ClientAuth/Email by mode) ServerAuth ✅ for our use
SAN types DNS · IP · email · URI DNS · IP ✅ sufficient; §3
Trust stores system · NSS (Firefox/Chrome) · Java system · NSS Java skipped (§3)
Client certs -client mode Not needed
Idempotent install / Exists yes yes (marker + NSS Exists) ✅ match
Cross-OS macOS · Linux · Windows same (via truststore); Linux proven, mac/win pinned ✅ code / ⏸ validation
Elevation sudo (terminal) inherits truststore's sudo ⚠️ §4

Correctness fix the spike surfaced

mkcert sets KeyUsageKeyEncipherment on leaves because it defaults to RSA (key transport). Our leaves are ECDSA, where key encipherment is meaningless — ECDHE-ECDSA uses the key only to sign the key exchange. We now set KeyUsageDigitalSignature alone (ca.go). Harmless in mkcert's RSA world; the right thing in ours.

3. Deliberate differences (all defensible)

  • ECDSA vs RSA default. mkcert defaults to RSA for the long tail of ancient clients; it offers ECDSA behind --ecdsa. Our consumers are modern browsers hitting a local studio + an OAuth loopback — ECDSA P-256 is universally trusted, smaller, faster. Keep ECDSA default. If a consumer ever hits an RSA-only client, a future Config.KeyAlgorithm knob (ECDSA default, RSA option) restores full mkcert parity — noted, not built.
  • 90-day auto-renewing leaves. mkcert issues long-lived leaves (no renewal machinery); we issue short leaves and re-mint on expiry/SAN-drift in EnsureServed. Ours is safely under every browser leaf-validity cap and needs no user action. Keep.
  • No Java store. Niche for a Go + browser tool. truststore.WithJava() exists if a consumer ever needs it — a one-line addition. Defer.
  • DNS/IP SANs only. email/URI SANs are for client-cert / service-identity use we don't have. Defer.

4. The elevation question (Linux terminal vs macOS dialog)

Finding: mkcert and truststore elevate the system-store install with sudo on both Linux and macOS. sudo prompts in the terminal. There is no desktop dialog in mkcert's flow — the "GUI password box" only appears if you use a GUI elevation mechanism, which mkcert does not.

Consequences per consumer:

  • keryx (CLI studio, launched from a terminal): works today. When localca installs the system root, sudo pauses and asks for the password in the same terminal. To make it clean we should: (a) log why the prompt is about to appear before calling install; (b) optionally pre-warm with sudo -v so the prompt is a single, expected moment; © fall back gracefully (NSS-only / self-signed) when there's no TTY.
  • krites (.dmg GUI, no terminal): sudo has nowhere to prompt and fails. A GUI app needs OS-native elevation that renders a desktop dialog:
  • macOS: osascript -e 'do shell script "…" with administrator privileges' → the native admin dialog. (Heavier options: a privileged helper via SMJobBless.)
  • Linux: pkexec <cmd> → a polkit GUI dialog when a polkit agent/desktop is running; otherwise it still needs a terminal.
  • Windows: a UAC-elevated re-exec (runas / a manifest requesting elevation).

truststore does not expose an elevation hook — it hard-codes CommandWithSudo and, on macOS, sudo security …. So to give krites a desktop dialog we must either:

  1. Add an Elevator seam in go/localca and do the system-store install ourselves for the GUI path (we already have the exact commands from truststore's source), choosing sudo / osascript / pkexec by consumer + environment; or
  2. Contribute an elevation hook upstream to smallstep/truststore; or
  3. Re-exec the whole tool elevated at the GUI layer (krites requests elevation once, then the child process installs with plain root — no per-command prompt).

Recommendation: keryx (CLI) ships now on truststore's sudo path — it already does exactly what's wanted (a terminal prompt as part of the flow). Defer the GUI Elevator seam to the krites work, where the desktop-dialog requirement is real; option (1) keeps it in our module and testable. This is a decision to confirm (see spec follow-up).

5. Sources

  • github.com/smallstep/truststore@v0.13.0truststore_linux.go, truststore_darwin.go, truststore_nss.go, errors.go (CommandWithSudo). Extracted mkcert trust logic.
  • github.com/FiloSottile/mkcertcert.go (RSA 3072 root / RSA 2048 leaf defaults, --ecdsa P-256, 10y root, ~2y3m leaf kept < 825d, DNS/IP/email/URI SANs, IsCA + MaxPathLenZero).