Summary
Replace github.com/zalando/go-keyring with github.com/keybase/go-keychain in internal/keychain/ and add a thin CGo wrapper to expose kSecUseDataProtectionKeychain. This is a reliability improvement on its own, and lays the groundwork for cross-app credential sharing with other Jamf tooling.
Motivation
Reliability — subprocess vs. Security.framework
zalando/go-keyring shells out to /usr/bin/security for every keychain operation and parses stdout. This is fragile: output format can vary across macOS versions, and it base64-encodes all stored values (prefixed go-keyring-base64:) to work around shell-escaping constraints. keybase/go-keychain calls Security.framework directly via CGo, which is more robust, faster, and stores values as plain UTF-8 bytes.
Cross-app credential sharing
Other Jamf tooling (e.g. apiutil) stores credentials using kSecAttrAccessGroup and kSecUseDataProtectionKeychain. Items created with an access group are invisible to apps that aren't in the same group — not even a permission prompt. For jamf-cli to read or share credentials with such tools, it needs:
- Direct Security.framework bindings that accept
kSecAttrAccessGroup — keybase/go-keychain provides this
kSecUseDataProtectionKeychain: true — required whenever an access group is set; not currently exposed by keybase/go-keychain, so needs a thin CGo wrapper
Agreeing on a shared access group + matching provisioning profile is a separate coordination step, but the code changes here are the prerequisite.
Proposed changes
1. Swap the dependency
go get github.com/keybase/go-keychain
go mod tidy # removes zalando/go-keyring
2. Rewrite internal/keychain/keychain.go
Update systemStore to use keybase/go-keychain instead of go-keyring. The public Store interface (Get, Set, Delete) and ParseRef / KeychainRef helpers are unchanged — callers see no difference.
3. Thin wrapper for kSecUseDataProtectionKeychain
keybase/go-keychain does not expose this constant. Add a small Darwin-only CGo file in internal/keychain/ that sets it when building items destined for an access group:
// keychain_darwin.go
// #cgo LDFLAGS: -framework CoreFoundation -framework Security
// #include <Security/Security.h>
import "C"
// setDataProtection adds kSecUseDataProtectionKeychain to a query dict.
// Required whenever kSecAttrAccessGroup is set.
For the initial implementation this wrapper can be a no-op/unexported — it just needs to exist and be correct before the access group work lands.
4. Migration: handle existing go-keyring-base64: encoded values
zalando/go-keyring stores all passwords with a go-keyring-base64: prefix (base64-encoded, to survive shell escaping). keybase/go-keychain reads raw bytes, so it will return the encoded string verbatim.
Migration must be transparent to users. On Get():
- Read the raw value
- If it starts with
go-keyring-base64:, base64-decode the suffix to recover the real secret
- Immediately re-
Set() the decoded value so the item is in the new format
- Return the decoded value
This one-time per-item migration fires silently on first use after upgrade. Users with existing config profiles referencing keychain: items will continue working without re-running config add-profile.
The legacy prefix go-keyring-encoded: (hex, older format) should also be handled the same way.
Platform impact
| Platform |
Before |
After |
| macOS |
subprocess /usr/bin/security |
CGo → Security.framework |
| Linux |
Secret Service D-Bus (pure Go) |
Secret Service D-Bus (pure Go) — unchanged |
| Windows |
Credential Manager |
(out of scope — not a target platform) |
CGo is gated to Darwin in keybase/go-keychain, so Linux builds remain CGo-free. macOS GitHub Actions runners (macos-latest) have Xcode/clang, so CGo builds work without any CI changes.
Cross-compilation (GOOS=darwin from Linux) will break for the keychain package unless a cross-compiler is configured — worth checking the release workflow.
Acceptance criteria
Summary
Replace
github.com/zalando/go-keyringwithgithub.com/keybase/go-keychainininternal/keychain/and add a thin CGo wrapper to exposekSecUseDataProtectionKeychain. This is a reliability improvement on its own, and lays the groundwork for cross-app credential sharing with other Jamf tooling.Motivation
Reliability — subprocess vs. Security.framework
zalando/go-keyringshells out to/usr/bin/securityfor every keychain operation and parses stdout. This is fragile: output format can vary across macOS versions, and it base64-encodes all stored values (prefixedgo-keyring-base64:) to work around shell-escaping constraints.keybase/go-keychaincalls Security.framework directly via CGo, which is more robust, faster, and stores values as plain UTF-8 bytes.Cross-app credential sharing
Other Jamf tooling (e.g. apiutil) stores credentials using
kSecAttrAccessGroupandkSecUseDataProtectionKeychain. Items created with an access group are invisible to apps that aren't in the same group — not even a permission prompt. For jamf-cli to read or share credentials with such tools, it needs:kSecAttrAccessGroup—keybase/go-keychainprovides thiskSecUseDataProtectionKeychain: true— required whenever an access group is set; not currently exposed bykeybase/go-keychain, so needs a thin CGo wrapperAgreeing on a shared access group + matching provisioning profile is a separate coordination step, but the code changes here are the prerequisite.
Proposed changes
1. Swap the dependency
2. Rewrite
internal/keychain/keychain.goUpdate
systemStoreto usekeybase/go-keychaininstead ofgo-keyring. The publicStoreinterface (Get,Set,Delete) andParseRef/KeychainRefhelpers are unchanged — callers see no difference.3. Thin wrapper for
kSecUseDataProtectionKeychainkeybase/go-keychaindoes not expose this constant. Add a small Darwin-only CGo file ininternal/keychain/that sets it when building items destined for an access group:For the initial implementation this wrapper can be a no-op/unexported — it just needs to exist and be correct before the access group work lands.
4. Migration: handle existing
go-keyring-base64:encoded valueszalando/go-keyringstores all passwords with ago-keyring-base64:prefix (base64-encoded, to survive shell escaping).keybase/go-keychainreads raw bytes, so it will return the encoded string verbatim.Migration must be transparent to users. On
Get():go-keyring-base64:, base64-decode the suffix to recover the real secretSet()the decoded value so the item is in the new formatThis one-time per-item migration fires silently on first use after upgrade. Users with existing config profiles referencing
keychain:items will continue working without re-runningconfig add-profile.The legacy prefix
go-keyring-encoded:(hex, older format) should also be handled the same way.Platform impact
/usr/bin/securityCGo is gated to Darwin in
keybase/go-keychain, so Linux builds remain CGo-free. macOS GitHub Actions runners (macos-latest) have Xcode/clang, so CGo builds work without any CI changes.Cross-compilation (
GOOS=darwinfrom Linux) will break for the keychain package unless a cross-compiler is configured — worth checking the release workflow.Acceptance criteria
zalando/go-keyringremoved fromgo.modkeybase/go-keychainadded;internal/keychain/rewritten to use itkSecUseDataProtectionKeychainwrapper exists (Darwin-only, even if not yet wired to a public API)Get()transparently migratesgo-keyring-base64:/go-keyring-encoded:prefixed valueskeychain:config profile references continue to work (no re-setup required)ParseRef/KeychainRef/DefaultServiceunchanged (no config format changes)