Date: 2026-02-21
| Component | Version |
|---|---|
| macOS | 15.6.1 (24G90) |
| Xcode | 26.2 (17C52) |
| rustc | 1.91.1 |
| cargo | 1.91.1 |
Can a plain Mach-O helper binary (mkp) access the macOS data protection keychain when:
- It is codesigned with a
keychain-access-groupsentitlement - It lives inside a signed
.appbundle that has a valid provisioning profile - The provisioning profile is embedded at
MyApp.app/Contents/embedded.provisionprofile
No. The provisioning profile only covers the main app executable, not helper binaries placed alongside it.
-
Signing
mkpwith entitlements + provisioning profile in the bundle →Killed: 9The kernel rejects the binary at launch. The restrictedkeychain-access-groupsentitlement requires a provisioning profile that covers the specific binary, andembedded.provisionprofiledoes not extend to helper tools. -
Signing
mkpwithout entitlements → runs, but keychain access failserrSecMissingEntitlement("A required entitlement isn't present.") when attempting to use the data protection keychain.
The data protection keychain is not usable from a helper binary inside an .app bundle unless the helper has its own provisioning profile (which plain Mach-O binaries cannot have).
- Use the legacy login keychain — call
PasswordOptions::new_generic_password()without.use_protected_keychain(). The login keychain does not require entitlements. - Perform keychain operations in the main app and relay results to the helper via IPC.
The app bundle structure:
MyApp.app/
Contents/
MacOS/
MyApp ← main executable (Xcode-built, has entitlements via profile)
mkp ← helper binary (plain Mach-O, cargo-built)
embedded.provisionprofile
Entitlements file (MyApp.entitlements):
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
"http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>keychain-access-groups</key>
<array>
<string>TEAM_ID.com.example.myapp</string>
</array>
</dict>
</plist>Makefile bundle target (simplified):
bundle: all
cp target/release/mkp $(APP_BUNDLE)/Contents/MacOS/
cp MyApp.provisionprofile $(APP_BUNDLE)/Contents/embedded.provisionprofile
codesign --force --options runtime \
--entitlements MyApp.entitlements \
--sign "Developer ID Application: ..." \
$(APP_BUNDLE)/Contents/MacOS/mkp
codesign --force --options runtime \
--entitlements MyApp.entitlements \
--sign "Developer ID Application: ..." \
$(APP_BUNDLE)Added to mkp/src/main.rs — a --keychain-test flag that writes, reads, and deletes a test item from the data protection keychain, then exits:
if keychain_test {
use security_framework::passwords::{
delete_generic_password_options, generic_password,
set_generic_password_options, PasswordOptions,
};
let service = "com.example.keychain-test";
let account = "test";
let test_data = b"hello-keychain";
let opts = || {
let mut o = PasswordOptions::new_generic_password(service, account);
o.use_protected_keychain();
o
};
// Write
match set_generic_password_options(test_data, opts()) {
Ok(()) => println!("WRITE: ok"),
Err(e) => {
println!("WRITE: FAILED — {e}");
std::process::exit(1);
}
}
// Read back
match generic_password(opts()) {
Ok(bytes) => {
if bytes == test_data {
println!("READ: ok (data matches)");
} else {
println!("READ: FAILED — data mismatch");
std::process::exit(1);
}
}
Err(e) => {
println!("READ: FAILED — {e}");
std::process::exit(1);
}
}
// Cleanup
match delete_generic_password_options(opts()) {
Ok(()) => println!("DELETE: ok"),
Err(e) => println!("DELETE: failed (non-fatal) — {e}"),
}
println!("Keychain test PASSED");
std::process::exit(0);
}$ make bundle
cp target/release/mkp .../MyApp.app/Contents/MacOS/
cp MyApp.provisionprofile .../MyApp.app/Contents/embedded.provisionprofile
codesign --force --options runtime --entitlements MyApp.entitlements \
--sign "Developer ID Application: ..." .../MyApp.app/Contents/MacOS/mkp
codesign --force --options runtime --entitlements MyApp.entitlements \
--sign "Developer ID Application: ..." .../MyApp.app
$ codesign -d --entitlements - .../MyApp.app/Contents/MacOS/mkp
[Dict]
[Key] keychain-access-groups
[Value]
[Array]
[String] TEAM_ID.com.example.myapp
$ .../MyApp.app/Contents/MacOS/mkp --keychain-test
Killed: 9The kernel kills the process at launch. The keychain-access-groups entitlement is restricted and requires a provisioning profile that covers this specific binary. The bundle's embedded.provisionprofile does not extend to helper executables.
$ codesign --force --options runtime \
--sign "Developer ID Application: ..." \
.../MyApp.app/Contents/MacOS/mkp
$ .../MyApp.app/Contents/MacOS/mkp --keychain-test
WRITE: FAILED — A required entitlement isn't present.The binary runs, but the data protection keychain rejects access because keychain-access-groups is not present.
| Signing | Entitlements | Result |
|---|---|---|
| Developer ID + entitlements | keychain-access-groups |
Killed: 9 (kernel rejects binary) |
| Developer ID, no entitlements | none | Runs, but errSecMissingEntitlement on keychain access |
The data protection keychain is not accessible from a helper binary inside an .app bundle. Use the legacy login keychain or perform keychain operations in the main app process instead.