You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
apple: AES-GCM-wrap the SE dataRepresentation under a Keychain key (#65)
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.
Co-authored-by: Jay Gowdy <jay@gowdy.me>
|**1**| macOS Secure Enclave |**The SE hardware.**`sshenc` sends data to the SE via CryptoKit; the SE performs ECDSA P-256 internally and returns the signature. The private key never exists outside the chip. Works for both signed and unsigned binaries on Apple Silicon. |**No** — impossible. Even root cannot extract it. The `dataRepresentation` on disk is an opaque SE handle (not key material); it is written with 0600 permissions today. AES-GCM wrapping under a Keychain-stored key is planned (see `fix-macos.md`) and not yet implemented. | Touch ID / biometric enforced by SE hardware per-signature (when access policy is set). | Secure Enclave coprocessor + handle file on disk (0600). Keychain-wrapped handle is a planned hardening. |
159
+
|**1**| macOS Secure Enclave |**The SE hardware.**`sshenc` sends data to the SE via CryptoKit; the SE performs ECDSA P-256 internally and returns the signature. The private key never exists outside the chip. Works for both signed and unsigned binaries on Apple Silicon. |**No** — impossible. Even root cannot extract it. The `dataRepresentation` on disk is an opaque SE handle (not key material); it is AES-256-GCM wrapped under a 32-byte key stored in the login Keychain (service `com.enclaveapp.<app>`, account `<label>`). File format `[EHW1 magic][nonce][ciphertext][tag]`. | Touch ID / biometric enforced by SE hardware per-signature (when access policy is set). | Secure Enclave coprocessor + Keychain-wrapped handle on disk (0600). |
160
160
|**2**| Windows TPM 2.0 |**The TPM hardware.** CNG sends signing requests to the TPM via NCrypt. |**No** — key is a non-exportable TPM object. | Windows Hello (biometric/PIN) enforced per-signature via `NCRYPT_UI_POLICY`. | TPM 2.0 chip. |
161
161
|**3**| Linux TPM 2.0 |**The TPM hardware.** Signing performed by the TPM via `tss-esapi`. |**No** — key is TPM-resident. | Not enforced (no standard Linux biometric API). | TPM 2.0 device (`/dev/tpmrm0`). glibc only. |
162
162
|**4**| Software (Linux glibc) |**Software.** The P-256 private key is decrypted from the keyring into memory and used for signing via the `p256` crate. |**Yes (encrypted at rest)** — P-256 private key on disk, encrypted via system keyring (D-Bus Secret Service / GNOME Keyring / KWallet). | Not enforced. |`~/.config/{app}/keys/` encrypted via keyring. |
@@ -170,7 +170,7 @@ The blast radius of encryption key compromise is further bounded by the expirati
170
170
171
171
| Level | Backend | Who decrypts? | Private key exportable? | User presence | Cached data protection |
|**1**| macOS Secure Enclave |**The SE hardware.** ECDH key agreement happens inside the SE. The shared secret is derived internally; only the AES-GCM decryption of the ciphertext body happens in software. Handle blob stored 0600 on disk; Keychain-wrapped handle is a planned hardening (see `fix-macos.md`). |**No** — impossible. | Touch ID / biometric can be required per-decrypt. | Ciphertext on disk can only be decrypted by the SE that created the key. Full disk access is insufficient without the SE. |
173
+
|**1**| macOS Secure Enclave |**The SE hardware.** ECDH key agreement happens inside the SE. The shared secret is derived internally; only the AES-GCM decryption of the ciphertext body happens in software. Handle blob is AES-256-GCM wrapped under a Keychain-held key (see `crates/enclaveapp-apple/src/keychain_wrap.rs`). |**No** — impossible. | Touch ID / biometric can be required per-decrypt. | Ciphertext on disk can only be decrypted by the SE that created the key. Full disk access is insufficient without the SE, and the Keychain wrapping key gates same-UID handle theft. |
174
174
|**2**| Windows TPM 2.0 |**The TPM hardware** performs ECDH. |**No** — TPM-bound. | Windows Hello can be required per-decrypt. | Only decryptable on the same machine's TPM. |
175
175
|**3**| Linux TPM 2.0 |**The TPM hardware** via `tss-esapi`. |**No** — TPM-bound. | Not enforced. | Same machine-binding as Windows TPM. |
176
176
|**4**| Software (Linux glibc) |**Software.** P-256 key decrypted from keyring. |**Yes (encrypted at rest)** — keyring-protected. | Not enforced. | Keyring-encrypted key protects cache at rest. |
@@ -182,11 +182,13 @@ All supported macOS hardware (Apple Silicon) has a Secure Enclave, and **CryptoK
182
182
183
183
The `com.apple.developer.secure-enclave` entitlement is a **Security.framework** concept for Keychain-stored SE keys (`SecItemAdd` with `kSecAttrTokenIDSecureEnclave`). Since libenclaveapp uses CryptoKit's `dataRepresentation` for key persistence (not Security.framework), the entitlement is not required.
184
184
185
-
**Handle protection for unsigned apps — planned, not yet implemented.** The SE's `dataRepresentation` is an opaque handle blob that allows the same device's SE to reconstruct the key reference. While the private key itself cannot be extracted from this blob, the blob is stored as a file on disk — and another process running as the same user could copy it and use it to request SE operations.
185
+
**Handle protection for unsigned apps.** The SE's `dataRepresentation` is an opaque handle blob that allows the same device's SE to reconstruct the key reference. While the private key itself cannot be extracted from this blob, the blob is stored as a file on disk — and another process running as the same user could copy it and use it to request SE operations.
186
186
187
-
**Current state.**`.handle` is written with 0600 permissions and no further wrapping (see `crates/enclaveapp-apple/src/keychain.rs`). A same-UID attacker can read the handle and replay it against the SE from another process.
187
+
**Implemented.**`generate_and_save_key` creates a fresh 32-byte AES-256 wrapping key per label, stores it in the login keychain as a `kSecClassGenericPassword` item (service `com.enclaveapp.<app>`, account `<label>`), AES-256-GCM encrypts the `dataRepresentation` under that key, and writes the sealed blob to `.handle` with the magic prefix `EHW1`. Format: `[magic(4)][nonce(12)][ciphertext][tag(16)]`. See `crates/enclaveapp-apple/src/keychain_wrap.rs`.
188
188
189
-
**Planned state** (see `fix-macos.md`). Wrap the `dataRepresentation` with **AES-256-GCM** using a Keychain-stored wrapping key. The Keychain's per-application access control then gates same-user handle theft:
189
+
Legacy plaintext `.handle` files are accepted by `load_handle` for transparent migration; they re-wrap on the next rotation. `delete_key` removes the keychain entry alongside the on-disk artifacts.
190
+
191
+
The Keychain's per-application access control then gates same-user handle theft:
190
192
191
193
-**Signed app (code-signed with Developer ID):** The Keychain ACL is bound to the app's code signature. Other apps cannot access the wrapping key. Handle files are protected against same-user attacks.
192
194
@@ -200,7 +202,7 @@ On macOS, the `enclaveapp-apple` backend auto-detects which path to use at runti
200
202
201
203
-**Signed/entitled (Level 1):** The app is code-signed with a Developer ID certificate and has the Secure Enclave entitlement. Keys are created inside SE hardware via `SecureEnclave.P256.Signing.PrivateKey` / `SecureEnclave.P256.KeyAgreement.PrivateKey`. The key material physically cannot be extracted. This is the production path for distributed binaries.
202
204
203
-
-**Unsigned/development (Level 4):** The app is not code-signed or lacks entitlements (typical during local development with `cargo build`). Keys are created via regular `CryptoKit.P256` (not SE-bound). The private key's `dataRepresentation` is currently written to disk as a 0600 `.handle` file without additional wrapping; Keychain-backed AES-GCM wrapping is planned (see `fix-macos.md`). Even today the keys are not hardware-bound in this mode.
205
+
-**Unsigned/development (Level 4):** The app is not code-signed or lacks entitlements (typical during local development with `cargo build`). Keys are created via regular `CryptoKit.P256` (not SE-bound). The private key's `dataRepresentation` is AES-256-GCM wrapped under a Keychain-held key and written to a 0600 `.handle` file. Even with the wrapping, the keys are not hardware-bound in this mode — the SE isn't in the path.
Copy file name to clipboardExpand all lines: THREAT_MODEL.md
+2-2Lines changed: 2 additions & 2 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -83,8 +83,8 @@ Unsafe FFI surfaces are trusted by design but fragile.
83
83
84
84
### Keychain and key-backend-specific risks
85
85
86
-
-**macOS `.handle` storage is currently plaintext (0600).** DESIGN.md describes AES-GCM wrapping under a Keychain-stored key as the handle-theft defense. That code is not yet merged (see `fix-macos.md`). Until it lands, a same-UID attacker can copy a`.handle` file and replay it against the SE from another process. The SE still refuses to export the private key itself.
87
-
-**Cross-binary Keychain access on macOS** for ad-hoc signed builds (Homebrew, `cargo build`) is controlled by binary hash; every rebuild invalidates the ACL and reprompts the user. This is the Keychain enforcing its ACL, not a vulnerability, but it becomes load-bearing once Keychain wrapping lands (see above).
86
+
-**macOS `.handle` storage is AES-256-GCM wrapped under a Keychain-held key.**`generate_and_save_key` creates a fresh 32-byte wrapping key per label, stores it in the login keychain as a `kSecClassGenericPassword` item (service `com.enclaveapp.<app>`, account `<label>`), and writes the AES-GCM-sealed SE `dataRepresentation` to `.handle` (magic `EHW1`, format `[magic][nonce][ciphertext][tag]`). A same-UID attacker who copies the`.handle` file still needs the keychain-held wrapping key to replay SE operations — and the keychain's code-signature-bound ACL blocks access from a different binary, prompting the user on first use of a rebuilt binary. Legacy plaintext `.handle` files are accepted transparently for migration; they upgrade to wrapped format on the next rotation. See `crates/enclaveapp-apple/src/keychain_wrap.rs`.
87
+
-**Cross-binary Keychain access on macOS** for ad-hoc signed builds (Homebrew, `cargo build`) is controlled by binary hash; every rebuild invalidates the ACL and reprompts the user. This is the Keychain enforcing its ACL — it's now load-bearing because the wrapping key is what gates same-UID handle theft (above). Trusted signing identities eliminate the per-upgrade prompt.
88
88
-**Keyring D-Bus peer trust.** The keyring backend talks to the session D-Bus Secret Service. A hostile session bus (another process running as the user that took over the bus) could intercept unlock / decrypt requests. Same-user already-compromised session; out of scope for the library.
0 commit comments