Skip to content

[draft] mitm-df: NameConstraints-scoped CA + file keystore (foundation)#262

Draft
myleshorton wants to merge 2 commits into
mainfrom
mitm-df-foundation
Draft

[draft] mitm-df: NameConstraints-scoped CA + file keystore (foundation)#262
myleshorton wants to merge 2 commits into
mainfrom
mitm-df-foundation

Conversation

@myleshorton
Copy link
Copy Markdown
Contributor

Draft — first slice of engineering#3482 (MITM-DomainFronting outbound for lantern-box). Posting early as a marker; not for review yet.

What's here

The foundational crypto piece — a CA generator whose RFC 5280 NameConstraints extension cryptographically restricts the DNS names it can sign for. This lands first, before any of the outbound machinery that uses it, because the security model rests on it and the package is independent enough to review in isolation.

  • `internal/mitmca/ca.go` — `CA` struct, `GenerateCA`, `LoadCA`, `SignLeaf`. Includes a Go-side pre-check rejecting out-of-subtree SNIs before the signer is touched (the cryptographic enforcement happens via the cert's `NameConstraints` extension; this is defense-in-depth for verifiers that don't enforce it).
  • `internal/mitmca/keystore.go` — `KeyStore` interface and the cross-platform `FileKeyStore` (PKCS#8 PEM, mode 0600, atomic rename). Hardware-backed implementations (Secure Enclave / Android StrongBox / TPM) satisfy the same interface and land in follow-ups behind build tags.
  • `internal/mitmca/ca_test.go` — table-driven coverage for the gotchas (`notexample.com` vs subtree `example.com`, `example.com.evil.com` not under `example.com`), unconstrained-CA rejection on `Load`, fresh-vs-reloaded sign equivalence, POSIX mode bits, idempotent `Erase`.
  • `docs/mitm-df/architecture.md` — design doc covering the full feature: component map, package layout, public Go types, sing-box outbound config schema, end-to-end request flow, the security properties as enforced by this code, the implementation order.

Not yet here (follow-ups, in rough order)

  1. `internal/mitmdf` — the outbound itself: TLS termination, fronts table, SPKI pinner, audit log, session loop.
  2. `option/mitmdf.go` — sing-box config schema for `type="mitm-df"`.
  3. Hardware-backed keystores: `keystore_darwin.go` (Secure Enclave), `keystore_android.go` (StrongBox), `keystore_windows.go` (TPM/NCrypt), `keystore_linux.go` (TPM via PKCS#11 if available).
  4. Flutter UI: opt-in toggle, per-platform CA install flow, audit-log viewer.
  5. Cross-repo: config-server push channel for fronts updates, weekly validation CI, kill-switch flag.

Why `NameConstraints`

The user installs a personal root CA. If unconstrained, it can sign certs for any domain — banks, government, healthcare, anything. `NameConstraints` cryptographically restricts the CA to a fixed list of DNS subtrees. On any verifier that enforces it (Chrome, Firefox, Android API 30+, iOS 13+, modern Safari/Windows), the CA is incapable of producing a fraudulent cert for any domain outside that list — even if the private key is exfiltrated. That converts the threat model from "unbounded MITM authority" to "scoped MITM authority for ~10 named CDN families."

See engineering#3482 (security comment) for the full threat-model walk and the explanation of why on-the-wire SNI byte-swap doesn't work (TLS transcript HMAC binding).

Test plan

  • `go test ./internal/mitmca/...` passes locally
  • `go vet ./internal/mitmca/...` clean
  • `go build ./...` (whole repo) clean
  • Reviewer sanity-check the subtree-matching corner cases in `permits()`
  • Confirm Go stdlib `x509.Verify` enforces our `NameConstraints` (the `TestSignLeaf_VerifiesUnderCAPool` test catches the happy path; the rejection path is a future test once we have a way to mint a non-permitted leaf bypassing our own check)

🤖 Generated with Claude Code

@garmr-ulfr
Copy link
Copy Markdown
Collaborator

garmr-ulfr commented May 14, 2026

@myleshorton FYI I already had Claude implement a MITM outbound to replicate what's missing
https://wdynhnkxvsdx.slack.com/archives/C054ZBXNVE0/p1778789570280169?thread_ts=1778774452.953679&cid=C054ZBXNVE0. I haven't pushed anything yet though.

@myleshorton
Copy link
Copy Markdown
Contributor Author

Oh sweet!! Yeah the only interesting thing my planning on this uncovered is the NameConstraints RFC so we can specify a targeted set of domains we can generate certs for.

First slice of the MITM-DomainFronting outbound from engineering#3482.
Lands the foundational crypto piece — a CA generator whose RFC 5280
NameConstraints extension cryptographically restricts the set of DNS
names it can sign for — before any of the outbound machinery that will
use it.

What's in:

- internal/mitmca/ca.go: CA struct, GenerateCA, LoadCA, SignLeaf, plus
  the in-Go pre-check that rejects out-of-subtree SNIs before even
  reaching the signer.
- internal/mitmca/keystore.go: KeyStore interface and the default
  FileKeyStore implementation (PKCS#8 PEM, mode 0600, atomic rename).
  Per-platform hardware-backed implementations (Secure Enclave /
  Android StrongBox / TPM) satisfy the same interface and will land in
  follow-ups behind build tags.
- internal/mitmca/ca_test.go: table-driven coverage for the constraint
  cases that matter — subdomain matching, subtree-suffix lookalikes
  ("notexample.com", "example.com.evil.com"), unconstrained-CA
  rejection on Load, fresh-vs-reloaded sign equivalence, file mode
  0o600 (POSIX), Erase idempotence.
- docs/mitm-df/architecture.md: design doc for the whole feature.
  Covers component map, package layout, public Go types, the
  sing-box outbound config schema, end-to-end request flow, and
  the security properties as enforced by this code.

Not yet in (follow-ups):

- internal/mitmdf: the outbound itself (TLS termination, fronts table,
  SPKI pinner, audit log, session loop).
- option/mitmdf.go: sing-box config schema for type="mitm-df".
- Hardware-backed keystores: keystore_darwin.go (Secure Enclave),
  keystore_android.go (StrongBox), keystore_windows.go (TPM/NCrypt),
  keystore_linux.go (TPM via PKCS#11 if available; fall back to file).
- Flutter UI: opt-in toggle, per-platform CA-install flow, audit-log
  viewer.
- Cross-repo: config-server push channel for fronts updates, weekly
  validation CI in lantern-cloud, kill-switch flag.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@myleshorton myleshorton force-pushed the mitm-df-foundation branch from 1bd646c to 8b6083d Compare May 14, 2026 22:26
@garmr-ulfr
Copy link
Copy Markdown
Collaborator

garmr-ulfr commented May 14, 2026

Part of the mitmca package is actually what I was missing and some of it is actually better than what my Claude did regarding CA. I'm having Claude now rebase on this and update the outbound to use it. When it's done I'll create a PR to merge into here.

PR #263 (mitmfront-outbound) lands the actual outbound under
protocol/mitmdf/ rather than internal/mitmdf/ — sibling to the
existing protocol/ transports (algeneva, lanturn, samizdat, unbounded,
water). Update the architecture doc to match:

- Component diagram: MITM box now labeled protocol/mitmdf
- Package layout block: mitmdf/ moves out from under internal/ to
  protocol/, with a one-line note explaining why mitmca stays under
  internal/ (private helper, consumed only by mitmdf)
- Implementation-order list: the three mitmdf paths now reference
  protocol/mitmdf/...
- Drop session.go from the layout (its responsibilities fold into
  outbound.go's serve goroutine — matches what #263 actually shipped)
- Add utls_preset.go to the layout with a note about the
  HelloCustom + ApplyPreset ALPN-inheritance detail that PR #263 caught
- Add test/e2e/mitmdf_test.go to the layout (#263 ships it; the
  previous draft of this doc deferred it)

No code changes; doc-only.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants