fix(secrets): pin keychain access group + explicit data-protection routing (#279)#290
fix(secrets): pin keychain access group + explicit data-protection routing (#279)#290muqsitnawaz wants to merge 1 commit into
Conversation
…uting (#279) DRAFT / NEEDS-VERIFICATION on an affected machine (reporter: Darwin 25.5.0). The -25293 (errSecAuthFailed) regression does NOT reproduce on this build (macOS 26.4, 25E246) so the fix could not be end-to-end verified here. Issue #279: `agents secrets create/add` fails immediately on macOS with OSStatus -25293 (errSecAuthFailed). The original `set` attached only a biometry SecAccessControl with no kSecUseDataProtectionKeychain and no kSecAttrAccessGroup. Empirical correction to the original hypothesis (probed on macOS 26.4): adding a biometry SecAccessControl already routes the item to the data-protection keychain (the file-based keychain cannot store biometry-gated items) -- a plain add returns 0 here, a biometry-ACL add returns -34018, a DP-keychain-only error. So the write was NOT on the file-based keychain; moving it there explicitly does not, by itself, change which keychain is used. What this change actually does: - Pin kSecAttrAccessGroup to the concrete application-identifier group (2HTP252L87.com.phnx-labs.agents-keychain). The embedded profile grants the entitlement as a wildcard (2HTP252L87.*) with no concrete default group, so an add that omits the access group relies on implicit default-group resolution. Pinning removes that dependency -- the most likely real fix for -25293. - State kSecUseDataProtectionKeychain explicitly on every SecItem call (set, get, get-batch, delete, has, list, migrate-acl, migrateInline) so routing is deterministic and consistent across read and write. - Mark items kSecAttrSynchronizable=false (device-local), matching the existing ThisDeviceOnly intent. - Forward migration: readItem queries the data-protection keychain first, then the legacy file-based keychain ONCE on a clean miss; get/get-batch then rewrite the value into the data-protection keychain (delete legacy + re-add). Per-item and self-retiring -- idempotent without a separate sentinel. delete/set clear both keychains so no stale legacy copy shadows the new item. Signing/entitlements unchanged (already correct). Helper compiles clean for both arch slices. The biometry-SecAccessControl-evaluated-headlessly path (the reporter's own hypothesis) remains the alternative suspect if the access-group pin does not resolve it on the affected machine. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
Independent review: SOUND — needs verification on an affected machine. Verified locally:
CI note: the Verification gap (why this stays a draft): the macOS 26.4 |
Fixes #279 (pending verification).
Symptom
agents secrets create <name>/agents secrets add ...fail immediately on macOS withFailed to write keychain item (OSStatus -25293)(errSecAuthFailed). Returns instantly, no biometric prompt, persists across reinstalls.Root-cause correction (with machine evidence)
The original debugging hypothesis was: the
setpath omitskSecUseDataProtectionKeychain, so it targets the legacy file-based login keychain, which macOS 26.4 broke (-25293); moving to the data-protection keychain dodges it.I probed the actual
SecItemAddrouting on macOS 26.4 (unsigned binary, so the data-protection keychain's entitlement check fires as a routing tell —-34018is a data-protection-keychain-only error; the file-based keychain has no entitlement model and returns0):SecItemAddattributes0kSecAttrAccessibleonly0kSecAttrAccessControl(what the originalsetused)-34018kSecUseDataProtectionKeychain-34018So the original
setwas already on the data-protection keychain — the biometrySecAccessControlforces it there (the file-based keychain cannot store biometry-gated items). The codebase's own existing comments already said as much (list/has: "items written bysetcarry a biometry ACL, which forces them into the data-protection keychain"). Apple's TN3137 documents biometry as "only supported by the data protection keychain."Therefore adding
kSecUseDataProtectionKeychainexplicitly does not change which keychain is used, and on its own most likely does not fix-25293.What this PR actually changes
kSecAttrAccessGroupto a concrete group —2HTP252L87.com.phnx-labs.agents-keychain(thecom.apple.application-identifierfrom the embedded profile). The profile grants the entitlement as a wildcard only (2HTP252L87.*) with no concrete default group, so an add that omits the access group depends on the system's implicit default-group resolution. Pinning a concrete group removes that dependency. This is the most plausible real cause of-25293and the change most likely to fix it.kSecUseDataProtectionKeychainexplicitly on everySecItemcall (set,get,get-batch,delete,has,list,migrate-acl,migrateInline) so routing is deterministic and consistent across reads and writes (items written to one keychain are invisible to queries against the other).kSecAttrSynchronizable = false— items stay device-local, matching the existing...ThisDeviceOnlyintent.readItemqueries the data-protection keychain first; on a clean miss (errSecItemNotFoundonly — never on an auth error) it reads the legacy file-based keychain once, andget/get-batchrewrite the value forward (delete legacy + re-add to DP with the biometry ACL + pinned group). Per-item and self-retiring: once migrated, the DP lookup hits and the fallback never fires again, so it is idempotent without a separate sentinel — the same pattern as the existing trusted-app-ACL JIT migration.set/deleteclear both keychains so no stale legacy copy shadows the new item.Signing/entitlements (
scripts/build-keychain-helper.sh,scripts/keychain-entitlements.plist, embedded provisioning profile) are unchanged — they were already correct, which is why the bug is-25293and not-34018on the reporter's signed binary.Apple references
kSecUseDataProtectionKeychainkSecUseDataProtectionKeychain/kSecAttrSynchronizable)Verification
Proven locally (macOS 26.4, 25E246):
arm64-apple-macos12,x86_64-apple-macos12) andlipointo a universal binary.-34018vs0table).bun run buildgreen; full test suite green (2373 passed, 51 skipped), incl. 13 source-level helper tests asserting DP routing, the pinned access group, dual-keychainset/delete, and the forward-migration logic.NOT proven locally (the verification gap):
-25293symptom does not reproduce on this build, so I cannot confirm the fix resolves it.keychain-access-groupsentitlement, which is only honored at runtime with the embedded provisioning profile (bin/embedded.provisionprofile, generated at release time, not in the repo). A Developer-ID-signed local binary carrying that entitlement without the profile is SIGKILLed by AMFI (confirmed: exit 137).Steps for the reporter (Darwin 25.5.0) to confirm:
APPLE_ID=… APPLE_APP_SPECIFIC_PASSWORD=… APPLE_TEAM_ID=2HTP252L87 ./scripts/build-keychain-helper.sh, thenbun run build.agents secrets create probe→ expect success (no-25293).agents secrets add probe TESTKEY --value hellothen read it back → expect a single Touch ID prompt and the value.-25293persists after the access-group pin, the remaining suspect is the biometrySecAccessControlevaluated at write time in the headless helper process (the reporter's own hypothesis in agents secrets: keychain write fails with OSStatus -25293 (errSecAuthFailed) on macOS — helper is correctly signed/notarized #279) — a different fix touching how the ACL is applied or how the helper is launched.🤖 Generated with Claude Code