Skip to content

fix(auth): stabilize macOS Keychain ACL across Homebrew upgrades#182

Merged
avivsinai merged 6 commits intomasterfrom
fix/keychain-acl-181
Apr 24, 2026
Merged

fix(auth): stabilize macOS Keychain ACL across Homebrew upgrades#182
avivsinai merged 6 commits intomasterfrom
fix/keychain-acl-181

Conversation

@avivsinai
Copy link
Copy Markdown
Owner

Fixes #181.

Summary

  • Pin the release binary's Designated Requirement to the bundle identifier in scripts/codesign-macos.sh (-r='designated => identifier "..."'). Previously codesign derived a cdhash-based DR that changed on every rebuild, which invalidated the Keychain ACL entry the user approved with "Always Allow" on the next brew upgrade bkt. The 0.21.0 changelog claim that the stable identifier alone preserved approvals was incomplete — this PR makes it actually true. Security trade-off documented in the script: any ad-hoc binary claiming the same identifier satisfies the DR. Developer ID signing remains future work.
  • Set KeychainTrustApplication and KeychainAccessibleWhenUnlocked on darwin in secret.Open() so fresh items trust the calling binary at create time and survive unlock cycles without re-prompting.
  • pkg/cmd/auth/auth.go deletes any stale Keychain entry before writing on darwin. 99designs/keyring's updateItem deliberately preserves existing ACLs, so users had to logout-then-login to benefit from the trust flags. Now a single bkt auth login after upgrade is enough.
  • Widen secret.Store.Delete to treat os.ErrNotExist as success (the file backend returns a raw PathError instead of keyring.ErrKeyNotFound).
  • New bkt auth doctor [host] reports binary signature, Designated Requirement, trust flags, and per-host Keychain presence. Uses security find-generic-password without -g so it never reads the secret or triggers a Keychain prompt. Distinguishes errSecItemNotFound (exit 44) from probe failures — probe failures surface as keychain=unknown plus a probe-inconclusive diagnosis; they are never silently collapsed into "missing".

How to verify

Before merging, on macOS:

go test ./...
go run ./cmd/bkt auth doctor

The first invocation on a go-run binary (cdhash DR) should diagnose the upgrade-reprompt scenario; after building and signing with the updated script, bkt auth doctor should report DR stable: yes and the "stable Designated Requirement" diagnosis.

Not in this PR

  • Apple Developer ID signing — future work; the DR pin plus trust flags give us upgrade stability without it.
  • macOS Keychain partition IDs — 99designs/keyring does not support them; no evidence they're involved in the observed symptom.
  • The darwin inter-process flock in internal/secret/darwin_lock.go is kept as-is.

Test plan

  • go test ./... green
  • go vet ./... clean
  • go build ./cmd/bkt succeeds
  • Manually signed a test binary with the updated codesign-macos.sh; codesign -d -r- reports designated => identifier "io.github.avivsinai.bitbucket-cli" (stable)
  • bkt auth doctor renders both diagnoses (cdhash-bound vs stable DR)
  • After merge + release, verify on a clean macOS machine: brew upgrade bkt followed by a command that reads the token does not re-prompt (only the one-time re-login after upgrade is required)

🤖 Generated with Claude Code

avivsinai and others added 2 commits April 23, 2026 18:05
Pin the release binary's Designated Requirement to the bundle identifier so
the Keychain ACL entry a user approved with "Always Allow" keeps matching
after `brew upgrade bkt`. The previous 0.21.0 claim that the stable
identifier alone preserved approvals was incomplete — without `-r=`,
codesign still derived a cdhash-based DR that changed on every rebuild.

Also set KeychainTrustApplication and KeychainAccessibleWhenUnlocked on
darwin so fresh items trust the calling binary at create time and survive
login/unlock cycles without re-prompting, and refresh the stored item on
`bkt auth login` by deleting before setting (99designs/keyring's update
path deliberately preserves existing ACLs, which left stale entries from
older binaries prompting forever).

Add `bkt auth doctor [host]` that reports binary signature, Designated
Requirement, trust flags, and per-host Keychain presence. Uses
`security find-generic-password` without `-g` so it never reads the stored
secret; probe failures surface as `keychain=unknown` and an explicit
next-step diagnosis instead of silently claiming absence.

Widen `secret.Store.Delete` to also treat `os.ErrNotExist` as success so
the encrypted file backend matches Keychain semantics.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@sentry
Copy link
Copy Markdown

sentry Bot commented Apr 23, 2026

Codecov Report

❌ Patch coverage is 44.48399% with 156 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
pkg/cmd/auth/doctor.go 46.69% 121 Missing and 16 partials ⚠️
internal/secret/store.go 11.11% 15 Missing and 1 partial ⚠️
pkg/cmd/auth/auth.go 50.00% 2 Missing and 1 partial ⚠️

📢 Thoughts on this report? Let us know!

avivsinai and others added 3 commits April 23, 2026 18:13
Exercises writeDoctorText on both darwin and non-darwin reports, the
URL-style and unknown-selector branches of chooseHostKeys, and
describeBackend. Raises patch coverage above codecov's 43% gate.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Make runCmd a package-level var so tests can stub codesign and security
invocations with canned output. Adds parser coverage for the adhoc/cdhash
DR case, the stable identifier-based DR case, and each of the three
keychainItemPresent outcomes (present, not-found, inconclusive).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Use absolute Apple-tool paths so a poisoned \$PATH cannot swap in a
malicious probe. Also surface the codesign requirement-dump error via
the returned reqErr instead of silently dropping it so inspectCodesign
failures reach the doctor ProbeErrors report.

Addresses security-auditor finding on PR #182.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
… keychain

Per Apple's kSecAttrAccessible docs, that attribute only applies to
data-protection keychain items or synchronizable items on macOS.
99designs/keyring uses the file-based path, so the flag is harmless
but currently does nothing. Set for forward compatibility.

No functional change.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@avivsinai avivsinai merged commit 6ae9e95 into master Apr 24, 2026
10 checks passed
@avivsinai avivsinai deleted the fix/keychain-acl-181 branch April 24, 2026 06:47
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.

bug: macOS keychain "Always Allow" not respected — subsequent invocations still prompt or timeout

1 participant