Spike: mkcert parity & the elevation question¶
- Date
- 16 July 2026
- Status
- INFORMATIONAL — findings feed the
go/localcadesign (spec2026-07-16-localca-module.md).
TL;DR¶
- Our install path is mkcert's.
go/localcawrapsgithub.com/smallstep/truststore, which is mkcert's trust-store code extracted verbatim into a library — every platform file carriesCopyright (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
sudoon 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 (osascripton macOS,pkexecon 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 futureConfig.KeyAlgorithmknob (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
localcainstalls the system root,sudopauses 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 withsudo -vso the prompt is a single, expected moment; © fall back gracefully (NSS-only / self-signed) when there's no TTY. - krites (
.dmgGUI, no terminal):sudohas 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 viaSMJobBless.) - 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:
- Add an
Elevatorseam ingo/localcaand do the system-store install ourselves for the GUI path (we already have the exact commands from truststore's source), choosingsudo/osascript/pkexecby consumer + environment; or - Contribute an elevation hook upstream to
smallstep/truststore; or - 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.0—truststore_linux.go,truststore_darwin.go,truststore_nss.go,errors.go(CommandWithSudo). Extracted mkcert trust logic.github.com/FiloSottile/mkcert—cert.go(RSA 3072 root / RSA 2048 leaf defaults,--ecdsaP-256, 10y root, ~2y3m leaf kept < 825d, DNS/IP/email/URI SANs,IsCA+MaxPathLenZero).