rdmsm4x changelog 20260925-0143: grok RTTy keychain prompt triage
Agent: grok@rdmsm4x/grok0925k (AGENT_LLM=grok). Scope: read-only triage on jdmbair13m5 (uid 502) + rdmsm4x; NO build/sign/upload/ASC changes (522 freeze); RTTy PID 20867 not touched.
Actions
- Inspected /Applications/RTTy.app on jdmbair13m5 (codesign, entitlements, profile, spctl, launchctl procinfo, unified log).
- Read code at tag v0.3.847-build522 (63229dc).
- Filed ISSUE-20260925-01 (bug, medium) against RTTy.
- Posted one bus message to all.
App Privacy check (522 code)
No developer or third-party analytics/crash/telemetry/license SDKs or endpoints. Network: user-chosen probe targets; speed.cloudflare.com /meta + /__down (public egress IP + speed test, no identifiers sent); CloudKit privateCloudDatabase only (fleet snapshot incl. device ID + HMAC egress fingerprint); LAN Bonjour/TLS peer publisher to paired peers. Egress HMAC and device ID are never sent to a developer server. Supports Data Not Collected.
Ticket body
SYMPTOM (jdmbair13m5, uid 502 richh): macOS repeatedly shows "Keychain Not Found - A keychain cannot be found to store "egress-hmac-secret-v1"" (Cancel / Reset To Defaults). Requester /Applications/RTTy.app PID 20867 (0.3.847/522, up ~17h, gui/502 Aqua session 100027 same as Finder).
SIGNING OF THAT COPY: Developer ID Direct build, not App Store/dev. Identifier com.eastcoastscience.rtty.direct; Authority "Developer ID Application: east coast science, llc (ZU2882L4HT)"; notarized + stapled (spctl: accepted, Notarized Developer ID); hardened runtime; timestamp 2026-09-24 07:12. Signed entitlements: application-identifier ZU2882L4HT.com.eastcoastscience.rtty.direct, team-identifier, iCloud CloudKit (Production). NO keychain-access-groups, NO app-sandbox (embedded "RTTy Direct Developer ID" profile would allow ZU2882L4HT.* but the signature does not claim it). Installed by the fleet deploy (script/deploy_preview_remote.sh: ditto → sudo mv → chown root:wheel → launchctl asuser open) via retry-jdmbair13m5-522.zsh on 2026-09-24 (receipt STATUS=accepted, pid 20867). No pkg receipt, no _MASReceipt.
EVIDENCE: unified log for RTTy[20867]: SecItemCopyMatching →
SecItemUpdate → SecItemAdd, each followed by "securityd MacOS error:
-25307" (errSecNoDefaultKeychain); 36 occurrences in the last 2h, two
SecItemAdd per ~2-4 min (e.g. 01:35:35, 01:35:38, 01:37:36, 01:37:42,
01:41:36, 01:41:38 EDT), one of them on the MAIN thread (4357cd). A
fresh process (security default-keychain -d user) sees
login.keychain-db as default and search list login + fleet-signing, so
the no-default condition is specific to the long-lived RTTy process
(Security framework caches the keychain search list/default per
process). Likely the list/default was transiently broken when RTTy
launched (same shape as ISSUE-20260921-24 on rdmsm4x, where the search
list lost login.keychain-db;
~/Library/Preferences/com.apple.security.plist was rewritten 2026-09-22
02:40 with login + fleet-signing and no explicit DefaultKeychain). Not
proven.
ROOT CAUSE IN CODE (tag v0.3.847-build522 = 63229dc, Sources/RTTy/Services/StableDeviceIdentityStore.swift, StableEgressHMACSecretStore):
- No kSecUseDataProtectionKeychain and no kSecAttrAccessGroup, so on macOS every SecItem* call goes to the legacy file-based keychain (default keychain / search list). LegacyIdentityMigrationPolicy declares the destination as .dataProtection in access group ZU2882L4HT.com.eastcoastscience.rtty.shared, but the store never uses it; the Direct signature also lacks keychain-access-groups, so data-protection with that group would fail with -34018 today anyway.
- saveToKeychain() REMOVES the LAContext(interactionNotAllowed) before SecItemAdd, so the add is interactive. The legacy SecKeychainAddItem path raises the "Keychain Not Found" SecurityAgent dialog on errSecNoDefaultKeychain; interactionNotAllowed would not suppress that dialog anyway (it only governs LocalAuthentication/ACL UI). kSecUseDataProtectionKeychain would.
- No negative cache/backoff: secret() is called from ObservationStore.currentFleetSnapshot() whenever publicEgressAddresses is non-empty (Cloudflare /meta succeeded), and currentFleetSnapshot() runs on every CloudKit projection publish and every peer snapshot request. load → notFound, generate, add → dialog → Cancel → throw → the whole snapshot fails → next cycle repeats. Hence the repeated prompt, plus a blocked main actor while the dialog is up, plus failed CloudKit/peer snapshots on that host.
- Project/build42/ws1-keychain-audit.md is stale/wrong: it says these stores use the "Data Protection Keychain" with a "non-interactive pattern"; the 522 code uses the file keychain and an interactive add.
APP REVIEW RISK: UNLIKELY (≈75-80% confidence it will NOT show). The Standard (App Store) target compiles the same file (StandardSources.txt line 20) and would also use the legacy keychain (sandbox entitlements at 63229dc: app-sandbox, app groups, CloudKit, network client/server, content filter; no keychain-access-groups). But the dialog needs errSecNoDefaultKeychain; a reviewer's clean Mac has a normal login keychain as default, a fresh install has no pre-existing item from another signer (so no ACL prompt either), and the add succeeds silently. Residual risk: reviewer account with odd keychain state, or sandbox-specific file-keychain behavior. Could not verify the uploaded build's signed entitlements: no 522 xcarchive/export on rdmsm4x (Archives empty; only _review/repair3-unsigned/RTTyStandard.xcarchive and dist/RTTy.pkg (Direct)).
WHAT RICH SHOULD DO ON THE AIR: Keep clicking Cancel (safe: nothing
is written, the snapshot just fails). Do NOT click "Reset To Defaults"
(it creates a new login keychain and moves the old one aside, losing
access to saved passwords until manually restored). Quit RTTy (menu bar
→ Quit) and relaunch it from /Applications; a fresh process should pick
up login.keychain-db as default (verify: no -25307 in
log show --process RTTy). If it recurs, check
security default-keychain -d user /
security list-keychains -d user.
PROPOSED FIX FOR THE NEXT BUILD (NOT NOW — App Store builds FROZEN while 0.3.847 (522) awaits submission; do not build/sign/upload/change ASC for this):
- Use kSecUseDataProtectionKeychain=true on all StableDeviceIdentityStore/StableEgressHMACSecretStore queries (read, update, add); set kSecAttrAccessGroup to the shared group only when the signature carries keychain-access-groups (add $(AppIdentifierPrefix)com.eastcoastscience.rtty.shared to RTTyDirect.entitlements and RTTyStandard.entitlements, confirm the profiles allow it) — otherwise omit the group (defaults to the app's own).
- Keep the non-interactive context on the add too (or none needed once on data-protection); never let a background path reach legacy keychain UI.
- Make the egress HMAC optional: on any keychain error, omit egressFingerprint and still publish the snapshot; add a per-process negative cache/backoff so a failing keychain is tried at most once per launch (or per hour).
- Move secret()/deviceID() off the main actor (as loadDeviceIdentityOffMainActor already does for deviceID).
- Migration: read legacy file-keychain item non-interactively once, copy to data-protection, per LegacyIdentityMigrationPolicy.
- Correct Project/build42/ws1-keychain-audit.md.
- Test: a unit test asserting the query dictionaries contain kSecUseDataProtectionKeychain; a contract test that entitlements include the access group; a manual check with the default keychain unset (no dialog, snapshot still published).