feat(apple): adapt the macOS client to ABI v2 — client identity + SPAKE2 PIN pairing
ci / rust (push) Has been cancelled

The pairing/renegotiation batch bumped the punktfunk/1 ABI to v2 and the host now
hard-rejects v1 Hellos (m3.rs), so streaming from the Mac was dead until the bundled
PunktfunkCore.xcframework is rebuilt — it is gitignored, so that is a per-checkout step:
bash scripts/build-xcframework.sh. The Swift wrapper itself was already adapted upstream;
this lands the app on top of it.

- ClientIdentityStore: persistent client identity in the login Keychain, presented on
  every connect so paired hosts recognize this Mac. Keychain access failure throws
  instead of regenerating (a fresh identity would silently un-pair this Mac from every
  --require-pairing host); a lost first-run race resolves toward the stored identity;
  pairing uses the strict loadForPairing() so a memory-only identity can't strand a
  ceremony.
- PairSheet: the SPAKE2 PIN ceremony, reachable from a host card's context menu and from
  the trust prompt's "Pair with PIN instead…" (which drops the live session first — the
  host's accept loop is sequential). Success pins the verified fingerprint and connects;
  an in-flight ceremony self-discards when the sheet is dismissed, so a late success
  can't pin + auto-connect behind the user's back. Wrong PIN and Keychain failures get
  distinct, actionable error text.
- Tests: identity unit tests; the full pairing ceremony + --require-pairing gate on
  loopback (test-loopback.sh arms a second host, parses its PIN from the log, and gives
  both hosts throwaway config homes — no more writes to the real ~/.config/punktfunk);
  remote pairing + pinned stream over the LAN (PUNKTFUNK_REMOTE_PIN, _PORT).

Validated live against the box: SPAKE2 ceremony with the host's arming PIN → verified
fingerprint → pinned + identified 720p60 session (host persisted the client identity);
first light 60/60 AUs decoded to pixels; vkcube on glass through the app.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-06-10 21:49:36 +02:00
parent 49d31b9cad
commit 0494e0200a
11 changed files with 485 additions and 32 deletions
+33 -6
View File
@@ -1,17 +1,44 @@
#!/usr/bin/env bash
# Loopback integration: a real punktfunk/1 host (synthetic source — pure protocol, runs fine on
# macOS) on 127.0.0.1, then the Swift integration tests against it through the xcframework.
# The m3 host serves exactly one session and exits; the trap is just for failure paths.
# Loopback integration: real punktfunk/1 hosts (synthetic source — pure protocol, runs fine on
# macOS) on 127.0.0.1, then the Swift integration tests against them through the xcframework.
# Two hosts: an open one (stream round trip) and one armed with --require-pairing (the PIN
# ceremony + pairing gate — its random PIN is parsed out of its log).
set -euo pipefail
cd "$(dirname "$0")/../.."
PORT="${PUNKTFUNK_LOOPBACK_PORT:-19778}"
PAIR_PORT="${PUNKTFUNK_PAIRING_PORT:-19779}"
cargo build --release -p punktfunk-host
target/release/punktfunk-host m3-host --port "$PORT" --source synthetic --frames 300 &
# Each host gets a throwaway config home: the pairing host persists a trust store
# (punktfunk1-paired.json, resolved from $HOME) and both mint an identity cert on first
# run — none of that belongs in the user's real ~/.config/punktfunk, and separate homes
# also keep the two first runs from racing on the same cert.pem.
CFG="$(mktemp -d "${TMPDIR:-/tmp}/punktfunk-loopback.XXXXXX")"
PAIR_LOG="$CFG/pairing-host.log"
mkdir -p "$CFG/open" "$CFG/paired"
trap 'kill "${HOST_PID:-}" "${PAIR_PID:-}" 2>/dev/null || true' EXIT
HOME="$CFG/open" XDG_CONFIG_HOME="$CFG/open/.config" \
target/release/punktfunk-host m3-host --port "$PORT" --source synthetic --frames 300 &
HOST_PID=$!
trap 'kill "$HOST_PID" 2>/dev/null || true' EXIT
HOME="$CFG/paired" XDG_CONFIG_HOME="$CFG/paired/.config" \
target/release/punktfunk-host m3-host --port "$PAIR_PORT" --source synthetic --frames 300 \
--require-pairing >"$PAIR_LOG" 2>&1 &
PAIR_PID=$!
sleep 1
PIN=""
for _ in $(seq 50); do
PIN="$(grep -oE 'pair: [0-9]+' "$PAIR_LOG" | head -1 | cut -d' ' -f2 || true)"
[ -n "$PIN" ] && break
sleep 0.2
done
if [ -z "$PIN" ]; then
echo "no arming PIN in the pairing host's log ($PAIR_LOG)" >&2
exit 1
fi
cd clients/apple
PUNKTFUNK_LOOPBACK_PORT="$PORT" swift test --filter LoopbackIntegrationTests
PUNKTFUNK_LOOPBACK_PORT="$PORT" PUNKTFUNK_PAIRING_PORT="$PAIR_PORT" PUNKTFUNK_PAIRING_PIN="$PIN" \
swift test --filter LoopbackIntegrationTests