Skip to content

feat: encrypt the WiFi transport with cert-pinned mutual TLS 1.3 (#16) - #151

Open
gcobc12677 wants to merge 4 commits into
peetzweg:mainfrom
gcobc12677:pr/wifi-tls-pairing
Open

feat: encrypt the WiFi transport with cert-pinned mutual TLS 1.3 (#16)#151
gcobc12677 wants to merge 4 commits into
peetzweg:mainfrom
gcobc12677:pr/wifi-tls-pairing

Conversation

@gcobc12677

@gcobc12677 gcobc12677 commented Jul 14, 2026

Copy link
Copy Markdown

Encrypt the WiFi transport: USB-bootstrapped trust + cert-pinned mutual TLS 1.3

Addresses #16.

Motivation

Today WiFi mode streams H.264 in the clear on the local network and accepts any Mac that connects — anyone on the same Wi-Fi can passively record the screen or inject input. This PR makes WiFi private and authenticated before it's promoted to a first-class mode. USB stays point-to-point and unchanged.

What it does

  • Pairing is anchored to the physical USB cable, not a typed code. The first time a device is cabled, the Mac and iPad exchange self-signed P-256 identities over a loopback-only usbmux channel (:9010). The iPad shows a one-time confirmation displaying the key fingerprint (Signal-style grouped digits). On accept, each side pins the peer's public key (SPKI) in the data-protection Keychain.
  • WiFi then runs mutual TLS 1.3 (:9001) authenticated by those pinned identities. No low-entropy secret is ever on the wire, so there's nothing to brute-force; an unpinned Mac fails the handshake.
  • Unauthenticated connections are rejected — WiFi is TLS-only, there is no plaintext fallback on the LAN.
  • Self-healing re-pairing. The USB bootstrap runs once per USB session and the iPad is the source of truth: it silently auto-accepts on an exact pin match (so routine plug-ins never prompt) and shows the confirmation sheet otherwise. "Forget"/"Reset identity" revoke symmetrically over the wire and drop the live session immediately; a stale pin also self-heals on a TLS rejection. Every forget/reconnect combination ends at a fresh confirmation.

Relationship to #16

The goal and acceptance criteria of #16 are met: WiFi is encrypted and authenticated, and a third machine on the LAN can neither view the stream nor connect.

The approach differs deliberately from the one sketched in the issue:

#16 suggested This PR Why
NWProtocolTLS with a PSK cert-pinned mutual TLS 1.3 Network.framework PSK is TLS-1.2-only on Apple platforms, and a PSK derived from a low-entropy code is offline-dictionary-attackable (TLS-PSK is not a PAKE). Pinned self-signed identities give TLS 1.3 + forward secrecy with no crackable secret.
Short numeric pairing code USB physical-trust bootstrap The cable is a stronger trust anchor than a low-entropy code: no offline-crackable secret, no remote attack surface during pairing. Mirrors Apple's own "Trust This Computer" model.

Tradeoff to flag: because pairing is USB-first, a device that is never cabled to a given Mac cannot pair over WiFi. If cable-less pairing is desired, a numeric-code path would need a real PAKE (e.g. SPAKE2) — happy to do it as a follow-up if you'd prefer to keep #16's original code-based flow.

Security properties

  • Screen content and input are encrypted end-to-end over WiFi (mutual TLS 1.3).
  • MITM-resistant: both ends present and verify pinned self-signed certs; private keys never leave the device.
  • The bootstrap listener is bound to 127.0.0.1 (usbmux only, unreachable from the LAN) and needs no Local Network permission.
  • Auto-accept only on an exact installID + SPKI match; a new or changed identity always requires on-screen confirmation.

Threat model is the local network. A malicious app already running on the iPad can reach the loopback USB ports (127.0.0.1:9000/:9010) — but on-device compromise is out of scope (such an app could screen-record by other means), and even then it can only DoS or spoof the local display, never read the Mac's screen or decrypt the WiFi session. The USB video channel stays plaintext by design: the cable + Apple's "Trust This Computer" is the physical trust boundary.

Compatibility & rollout

  • USB unaffected: still plaintext over a loopback-bound port (physical channel); the :9000 listener just stops advertising on the LAN. A version-mismatched (pre-TLS) peer still streams over USB — only WiFi requires the newer protocol.
  • Discovery reuses the existing _opensidecar._tcp Bonjour type — no new service type is declared, so upgrading installs never need to re-grant Local Network permission (macOS snapshots declared Bonjour types at grant time). The TLS listener advertises on that type; the Mac dials the discovered endpoint (which resolves to the TLS port).
  • WiFi is TLS-only: pre-TLS peers can't establish a WiFi session (they can't pair or complete the pinned handshake), so they're USB-only by construction rather than by a hard version gate.
  • Deployment targets unchanged (macOS 14 / iOS 17).

Dependencies

Adds apple/swift-certificates and apple/swift-asn1 (both targets) for minting/serializing the self-signed identities.

New files

  • Shared/TrustStore.swift — identity generation, Keychain-backed pins, in-memory pin snapshot, fingerprint.
  • Shared/TLSConfigurator.swift — pinned mutual-TLS 1.3 NWProtocolTLS.Options.
  • Mac/TrustBootstrapClient.swift — the USB :9010 identity exchange.

Testing

  • Both schemes build (macOS + iOS, deployment targets 14/17).
  • Verified on device (macOS 26.2 + iPadOS 26.5): first pairing shows the fingerprint sheet and connects; a packet capture on :9001 confirms TLS application-data records (0x17 0x03 …) with no plaintext H.264/JSON on the wire; seamless USB ⇄ WiFi transport switching; Forget / Reset identity revoke and re-pair correctly (both sides, connected and disconnected) while routine re-plugs don't re-prompt.

Note: on-device runtime was exercised on macOS 26 / iPadOS 26; the self-signed SecIdentity formation path is compile-checked against the 14/17 SDK settings but hasn't been runtime-verified on 14/17 hardware.

Not included

  • The dev-only signing convenience (gitignored Signing.xcconfig) and any personal bundle identifier are intentionally kept out of this PR.

🤖 Generated with Claude Code

…tual TLS 1.3

Adds an authenticated, encrypted WiFi path so screen/input traffic is no longer
sent in the clear on the local network (issue peetzweg#16).

Trust model — no pairing code; the USB cable is the physical trust anchor:
- First time a device is cabled, the Mac and iPad exchange self-signed P-256
  identities over a loopback-only usbmux channel (:9010); the iPad shows a
  one-time confirmation with the key fingerprint. Both persist the peer's
  public key (SPKI) in the data-protection Keychain.
- WiFi then uses mutual TLS 1.3 (:9001) with those pinned identities — no
  low-entropy secret, immune to offline attack, MITM-resistant.
- The USB bootstrap is self-verifying: it runs once per USB session and the
  iPad decides (silent auto-accept on an exact pin match, prompt otherwise),
  so re-pairing works even when one side has forgotten the other.
- Forget/reset revokes symmetrically over the wire and drops the live session.

Transport is TLS-only over WiFi (no plaintext fallback); USB streaming stays
plaintext on a loopback-bound port (physical channel). Discovery reuses the
existing _opensidecar._tcp Bonjour type (no new type → no Local Network
re-grant on upgrade). Adds apple/swift-certificates + swift-asn1.

New: Shared/{TrustStore,TLSConfigurator}.swift, Mac/TrustBootstrapClient.swift.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@gcobc12677 gcobc12677 changed the title Encrypt the WiFi transport: USB-bootstrapped trust + cert-pinned mutual TLS 1.3 feat: encrypt the WiFi transport with cert-pinned mutual TLS 1.3 (#16) Jul 14, 2026
gcobc12677 and others added 3 commits July 14, 2026 19:36
Addresses review of peetzweg#151.

- USB no longer gated on the WiFi protocol floor. minSupportedPeer returns to 1
  (USB is plaintext over a physical loopback channel, and iOS/Mac update
  independently, so a raised floor would strand working USB setups mid-rollout).
  WiFi stays cert-pinned-TLS-only by construction — a pre-TLS peer can't pair
  or establish a WiFi session regardless of the floor. Add minWiFiPeer (3) as
  an informational constant driving a soft "update to use Wi-Fi" hint, never a
  connection gate.
- Rewrite code comments that referenced an internal design doc (removed
  SEV-N / plan §N / wifi-tls-pairing-plan pointers) to be self-contained; kept
  real references (peetzweg#16, peetzweg#52, peetzweg#132, TN3137).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Review nits from peetzweg#151 (comment-only, no behavior change):
- Document that the phone's wire identity is PhoneReceiver.installID
  (UserDefaults) and it deliberately never uses TrustStore.installID()
  (that Keychain-backed id is the caller's own self-id, read only by the Mac).
- Refresh three "next milestone" comments that describe already-wired behavior
  (reinstall-cleanup flag and the Paired Macs UI are implemented on this branch).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1. autoConnect resolves the WiFi peer via the installIDByWifiName fallback (like connect()), so a paired device whose browse result lacks the TXT id still auto-reconnects over WiFi instead of being wrongly told to connect via USB (peetzweg#2).
2. resolvePeerID now reads the in-memory snapshot via a new peerID(forSPKI:) lookup instead of N+1 Keychain reads on the video queue; the snapshot carries (peerID, SPKI) pairs and allPinnedPeerSPKIs maps to the SPKIs the verify block already used (peetzweg#4).
3. Clear the stale "connect via USB once" caption when a USB bootstrap pins a device (peetzweg#5).
4. Document that a loopback (usbmux) connection intentionally preempts WiFi TLS and that on-device loopback callers are out of scope — comment-only, no behavior change (peetzweg#1/peetzweg#3).

Co-Authored-By: Claude Opus 4.8 (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.

1 participant