apple: AES-GCM-wrap the SE dataRepresentation under a Keychain key#65
Merged
jgowdy-godaddy merged 1 commit intomainfrom Apr 17, 2026
Merged
apple: AES-GCM-wrap the SE dataRepresentation under a Keychain key#65jgowdy-godaddy merged 1 commit intomainfrom
jgowdy-godaddy merged 1 commit intomainfrom
Conversation
Implements Steps 1-6 of fix-macos.md: close the same-UID `.handle`
theft threat that made the SE backend's security promise leaky on
Homebrew / cargo-install distribution.
Before this change: the SE `dataRepresentation` returned by CryptoKit
was written to `<label>.handle` as plaintext (0600). Same-UID attacker
copies the file → can drive SE operations as the user from another
process. SE key itself still never exports, but that distinction is
useless when the attacker can force SE operations at will.
After this change:
1. Swift bridge gains keychain_store / keychain_load / keychain_delete
helpers that wrap kSecClassGenericPassword items in the login
keychain. Uses legacy (not Data Protection) keychain because the
modern one fails with -34018 on unsigned builds.
2. Rust FFI declarations for the three new helpers.
3. New module keychain_wrap.rs with:
- WRAP_MAGIC (b"EHW1") + 32-byte key + 12-byte nonce + 16-byte tag
file format
- generate_wrapping_key / encrypt_blob / decrypt_blob using
aes-gcm + rand::rngs::OsRng
- keychain_store / keychain_load / keychain_delete thin wrappers
around the FFI
4. generate_and_save_key: generates a wrapping key, stores it in the
keychain, AES-GCM-encrypts the dataRepresentation, and persists
the sealed blob. On any failure after the SE key is minted, rolls
back the SE key + keychain entry so labels stay reusable.
5. load_handle: detects EHW1 magic and unwraps via the keychain key;
falls back to returning raw bytes for legacy plaintext `.handle`
files (transparent backward-compat migration — no on-disk rewrite
required to keep existing keys working).
6. delete_key: also removes the keychain wrapping-key entry.
Best-effort — a stale entry without its handle is inert.
## Testing
29 tests covering the new code:
- 20 pure-Rust unit tests for AES-GCM wrap/unwrap: empty / short /
long plaintext, magic prefix, round-trip consistency across
independent calls, tamper detection on ciphertext / tag / nonce,
truncation, wrong key, missing magic, legacy-plaintext rejection.
- 9 real-Keychain integration tests (macOS only, run by default):
basic round-trip, missing-entry returns None, store is idempotent
upsert, delete is idempotent, delete actually removes,
per-label isolation, per-app isolation, full wrap + keychain +
unwrap lifecycle, swapped-wrapping-key decrypt fails.
All tests use unique test-keyed service/account pairs and a RAII
KeychainEntryGuard so a failing test never leaks a keychain entry.
## Scope / deferred
Steps 7-8 (entitled Path 1 — SecKeyCreateRandomKey with
kSecAttrTokenIDSecureEnclave) are deferred. They require a
provisioning profile that self-signed and ad-hoc-signed binaries
cannot obtain; testing the entitled path needs a signed build with
AMFI-approved profile. For distribution via Homebrew / cargo
install, Path 2 (this PR) is sufficient.
THREAT_MODEL, DESIGN, fix-macos.md all updated to match the new
reality.
jgowdy-godaddy
pushed a commit
that referenced
this pull request
Apr 17, 2026
fix-macos.md's implementation plan is complete: - Steps 1-6 (Path 2: AES-GCM-wrapped .handle + keychain-held wrapping key) are shipped in PR #65. - Steps 7-8 (Path 1: entitled SecKeyCreateRandomKey with kSecAttrTokenIDSecureEnclave) are blocked on a provisioning profile, not deferred work. The required keychain-access-groups entitlement is AMFI-restricted and unavailable to Homebrew / cargo-install distribution, so these steps will not land under the current distribution model. The still-useful content from the plan doc has been migrated into THREAT_MODEL's macOS platform-specific notes: - Full prompt-behavior matrix (ad-hoc vs self-signed vs trusted-cert, first run / rebuild / different path) - Deny / Always Allow / upgrade-transition behavior - -34018 finding explaining why the legacy keychain is used - Explicit note that the entitled SE path is blocked on provisioning fix-macos.md is deleted.
2 tasks
jgowdy-godaddy
added a commit
that referenced
this pull request
Apr 17, 2026
fix-macos.md's implementation plan is complete: - Steps 1-6 (Path 2: AES-GCM-wrapped .handle + keychain-held wrapping key) are shipped in PR #65. - Steps 7-8 (Path 1: entitled SecKeyCreateRandomKey with kSecAttrTokenIDSecureEnclave) are blocked on a provisioning profile, not deferred work. The required keychain-access-groups entitlement is AMFI-restricted and unavailable to Homebrew / cargo-install distribution, so these steps will not land under the current distribution model. The still-useful content from the plan doc has been migrated into THREAT_MODEL's macOS platform-specific notes: - Full prompt-behavior matrix (ad-hoc vs self-signed vs trusted-cert, first run / rebuild / different path) - Deny / Always Allow / upgrade-transition behavior - -34018 finding explaining why the legacy keychain is used - Explicit note that the entitled SE path is blocked on provisioning fix-macos.md is deleted. Co-authored-by: Jay Gowdy <jay@gowdy.me>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Implements Steps 1-6 of `fix-macos.md`: close the same-UID `.handle` theft threat that made the SE backend's security promise leaky on Homebrew / cargo-install distribution.
Before: the SE `dataRepresentation` returned by CryptoKit was written to `.handle` as plaintext (0600). A same-UID attacker could copy the file and drive SE operations as the user from another process. The SE key itself still never exported, but that's useless when an attacker can force SE operations at will.
After: the handle is AES-256-GCM sealed under a 32-byte key held in the login keychain. Reading the handle is now gated by the keychain's code-signature-bound ACL — access from a different binary prompts the user; access after a rebuild (new code hash) prompts once and persists until the next rebuild.
What landed
Testing
29 tests covering the new surface:
Each integration test uses a test-unique service/account pair and a RAII `KeychainEntryGuard` so a failing test never leaks a keychain entry.
Scope / deferred
Steps 7-8 (entitled Path 1 — `SecKeyCreateRandomKey` with `kSecAttrTokenIDSecureEnclave`) are deferred. They require a provisioning profile that self-signed and ad-hoc-signed binaries cannot obtain; testing the entitled path needs a signed build with AMFI-approved profile. For Homebrew / cargo-install distribution (the common case), Path 2 (this PR) is sufficient.
`THREAT_MODEL.md`, `DESIGN.md`, and `fix-macos.md` all updated to reflect the new reality.
Test plan