The client identity prompted for Keychain access on every launch/rebuild. Root
cause: the macOS app target was ad-hoc signed (CODE_SIGN_IDENTITY = "-"), and
the identity lived in the file keychain whose "Always Allow" ACL is bound to the
app's exact code signature (cdhash for ad-hoc). Every rebuild changed the binary
-> changed the cdhash -> the ACL no longer matched -> re-prompt.
- Sign the macOS target with Apple Development (team already set) instead of
ad-hoc, so the designated requirement is identity-based and stable across
rebuilds.
- Move the identity to the data-protection keychain (kSecUseDataProtectionKeychain)
gated by a team-scoped keychain-access-group entitlement — access is granted by
the app's entitlement, not a per-binary ACL, so it's prompt-free and survives
rebuilds. Add Config/Punktfunk.entitlements and wire CODE_SIGN_ENTITLEMENTS into
all six app configs (macOS/iOS/tvOS).
- Unsigned / ad-hoc builds (e.g. `swift run`) lack the entitlement
(errSecMissingEntitlement) — fall back to the legacy file keychain so they still
work (with the old prompt), no hard failure.
macOS re-mints the identity on first run (the old file-keychain copy isn't in the
data-protection keychain) -> one re-pair, which is acceptable. iOS keeps its
identity (the explicit access group equals the prior default).
Validated: swift build; swift test (39 passed, 0 failures); xcodebuild
-showBuildSettings confirms Apple Development + Config/Punktfunk.entitlements.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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>