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
docs: close DESIGN.md gaps — layout, levels, process hardening, bridge, cache (#66)
Sweep of DESIGN.md against the code on main:
- Workspace layout diagram was missing enclaveapp-app-adapter,
enclaveapp-cache, enclaveapp-tpm-bridge, enclaveapp-build-support.
Added.
- 'Three integration types' while the following paragraphs define
four. Fixed; Type 4 (CredentialSource) was already described.
- Level 5 'Linux musl plaintext' shown as a production backend on
two security-level tables. Actually the only plaintext backend is
enclaveapp-test-software (explicitly marked 'NOT for production'),
and CLAUDE.md states musl is not supported. Tables now show
Level 4 as the terminal glibc-keyring row and reference the
test-only crate as an unnumbered out-of-band entry.
- 'macOS signed vs. unsigned' section described an auto-detecting
two-path runtime that doesn't exist. There is one code path;
SE is always in it; Path 1 (entitled) is deferred per fix-macos.md.
Rewrote as 'macOS path in practice (signed and unsigned)' framed
around Keychain prompt UX.
New sections documenting features that were in code but not in DESIGN:
- Process hardening (harden_process, PR_SET_DUMPABLE,
PR_SET_NO_NEW_PRIVS, RLIMIT_CORE=0, mlock_buffer).
- Shared infrastructure crates (app-adapter, cache, tpm-bridge,
build-support).
- WSL bridge discovery (fixed paths only; PATH fallback was removed;
64 KB cap; ENCLAVEAPP_BRIDGE_TIMEOUT_SECS; BridgeSession::Drop).
- Credential cache file tamper (consumer-layer max(header, config)
mitigation; AAD binding deferred).
Consumer mapping table expanded with shipped binary names, including
gitenc and npxenc which were previously invisible.
Co-authored-by: Jay Gowdy <jay@gowdy.me>
enclaveapp-tpm-bridge/ Shared TPM bridge server (JSON-RPC stdio, used by awsenc/sshenc/sso-jwt)
38
+
enclaveapp-cache/ Shared binary cache file format (magic + length-prefixed blobs)
39
+
enclaveapp-build-support/ Shared build.rs helpers (Windows PE resource compilation)
36
40
enclaveapp-test-support/ Mock backend for tests
37
41
```
38
42
@@ -70,9 +74,29 @@ It also centralizes WSL bridge lookup, access-policy handling, and app-specific
70
74
71
75
### WSL support
72
76
73
-
-`enclaveapp-bridge` defines the JSON-RPC bridge client and protocol
77
+
-`enclaveapp-bridge` defines the JSON-RPC bridge **client** and wire protocol
78
+
-`enclaveapp-tpm-bridge` is the shared JSON-RPC bridge **server** (runs natively on Windows; parameterized by `app_name` / `key_label`). `awsenc-tpm-bridge`, `sshenc-tpm-bridge`, and `sso-jwt-tpm-bridge` are thin wrappers over it.
Beyond the backends, a handful of crates provide cross-consumer utilities:
84
+
85
+
-**`enclaveapp-app-adapter`** — generic secret-delivery substrate used by Type 1-3 apps. Provides `BindingStore`, `SecretStore`, the program resolver, the `execve()`-based launcher (with `mlock` + zeroize of env-override bytes), provenance tracking, state-locking, and `TempConfig::write` (with the per-platform `create_platform_config()` memfd/tempfile selection).
86
+
-**`enclaveapp-cache`** — the shared on-disk cache file format (`[magic][version][flags][length-prefixed blobs]`). Consumed by sso-jwt's token cache and awsenc's credential cache.
87
+
-**`enclaveapp-tpm-bridge`** — the shared bridge server crate; delegated to by the per-app bridge binaries.
88
+
-**`enclaveapp-build-support`** — factored-out helpers for Windows `build.rs` resource compilation.
89
+
90
+
### Process hardening
91
+
92
+
`enclaveapp_core::process::harden_process()` is called as the first line of every enclave app binary's `main()`. It applies, best-effort (failures warn but don't abort):
93
+
94
+
-`setrlimit(RLIMIT_CORE, 0)` on all Unix — no core dumps that could capture secret buffers.
95
+
-`prctl(PR_SET_DUMPABLE, 0)` on Linux — `/proc/<pid>/mem` becomes root-only, `ptrace` attach from same-UID peers is denied.
96
+
-`prctl(PR_SET_NO_NEW_PRIVS, 1)` on Linux — subsequent `exec*()` can't gain setuid/file-capabilities privileges.
97
+
98
+
See `crates/enclaveapp-core/src/process.rs`. `mlock_buffer` / `munlock_buffer` are exposed for consumer crates that want to pin specific byte buffers in RAM.
99
+
76
100
## Access policy model
77
101
78
102
All backends share the same policy vocabulary:
@@ -159,8 +183,8 @@ Signing keys are long-lived identity keys (e.g., SSH keys). At Levels 1-3, the h
159
183
|**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
184
|**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
185
|**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
-
|**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. |
163
-
|**5**|Software (Linux musl) |**Software.**Private key read from disk into memory. |**Yes (plaintext on disk)** — P-256 private key stored as a file with 0o600 permissions. | Not enforced. |`~/.config/{app}/keys/` plaintext. |
186
+
|**4**| Software (Linux glibc, keyring) |**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. |
187
+
|**—**|`enclaveapp-test-software`|**Software, plaintext.**Test-only. Not used in any shipped binary. Never selected at runtime. |Yes (plaintext). | Not enforced. | Exists only to exercise the trait plumbing in unit tests. Linux musl builds are **not a supported production target** for libenclaveapp. |
164
188
165
189
#### Encryption key security
166
190
@@ -173,8 +197,8 @@ The blast radius of encryption key compromise is further bounded by the expirati
173
197
|**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
198
|**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
199
|**3**| Linux TPM 2.0 |**The TPM hardware** via `tss-esapi`. |**No** — TPM-bound. | Not enforced. | Same machine-binding as Windows TPM. |
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. |
177
-
|**5**|Software (Linux musl) |**Software.**P-256 key read from disk. |**Yes (plaintext on disk)** — 0o600 permissions only. | Not enforced. |Cache protection relies entirely on filesystem permissions. |
200
+
|**4**| Software (Linux glibc, keyring) |**Software.** P-256 key decrypted from keyring. |**Yes (encrypted at rest)** — keyring-protected. | Not enforced. | Keyring-encrypted key protects cache at rest. |
201
+
|**—**|`enclaveapp-test-software`|**Software, plaintext.**Test-only; not selected at runtime. | Yes (plaintext). | Not enforced. |Exists to exercise the trait plumbing in unit tests. Linux musl is not a supported production target. |
178
202
179
203
#### macOS: Secure Enclave access and key persistence
180
204
@@ -196,17 +220,19 @@ The Keychain's per-application access control then gates same-user handle theft:
196
220
197
221
In both cases, the SE performs all ECDSA signing and ECDH key agreement. The AES-256-GCM wrapping layer protects the persistence of the SE handle, not the cryptographic operations themselves.
198
222
199
-
#### macOS signed vs. unsigned
223
+
#### macOS path in practice (signed and unsigned)
200
224
201
-
On macOS, the `enclaveapp-apple` backend auto-detects which path to use at runtime:
225
+
There is one code path. `CryptoKit`'s `SecureEnclave.P256.*.PrivateKey` APIs work **without** a Developer ID cert or entitlements, and the SE always performs all ECDSA and ECDH operations regardless of signing state. The signing identity only affects the login-keychain UX for the AES-256-GCM wrapping key (see the previous section):
202
226
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.
227
+
-**Ad-hoc signed** (default from `swiftc` / `rustc`, `cargo build`, `brew install`): one "Always Allow" prompt per binary rebuild at the same path. Silent until the next upgrade.
228
+
-**Trusted signing identity** (e.g. Apple Developer ID): zero prompts across rebuilds — the keychain scopes access by identity, not hash.
229
+
-**Different binary at a different path**: always prompted, regardless of signing.
204
230
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.
231
+
A second "entitled" path (`SecKeyCreateRandomKey` with `kSecAttrTokenIDSecureEnclave` + `kSecAttrIsPermanent`, storing the SE key directly in the Keychain and eliminating the `.handle` file) is **blocked on distribution**. The required `keychain-access-groups` entitlement is AMFI-restricted and needs a provisioning profile — unavailable to Homebrew, `cargo install`, and ad-hoc / self-signed binaries. See `THREAT_MODEL.md` for details. For the distribution models libenclaveapp targets, the Path-2 wrapping already closes the same-UID `.handle` theft threat.
206
232
207
-
#### Linux glibc vs. musl
233
+
#### Linux
208
234
209
-
Glibc builds can access the system keyring (D-Bus Secret Service / GNOME Keyring / KWallet) to encrypt private key files at rest (Level 5). Musl builds (Alpine, static binaries) have no keyring and store keys as plaintext files with 0o600 permissions (Level 6). Both can use TPM 2.0 when available (glibc only, via `tss-esapi`), which upgrades to Level 3.
235
+
Glibc builds with the system keyring (D-Bus Secret Service / GNOME Keyring / KWallet) use the `enclaveapp-keyring` backend to encrypt private key files at rest (Level 4). Glibc builds also use TPM 2.0 via `tss-esapi` when it's available (Level 3). Linux musl is not a supported production target; the `enclaveapp-test-software` plaintext backend exists only for unit-test plumbing.
210
236
211
237
### Windows shell environments
212
238
@@ -248,7 +274,7 @@ Enclave apps are native binaries and work under any shell that can invoke execut
248
274
249
275
## Application integration types
250
276
251
-
Every enclave app is classified by how it delivers secrets to the target application. The `enclaveapp-app-adapter` crate defines three integration types, listed from most secure to least secure:
277
+
Every enclave app is classified by how it delivers secrets to the target application. The `enclaveapp-app-adapter` crate defines four integration types, listed from most-controlled to least-controlled:
252
278
253
279
### Type 1: HelperTool
254
280
@@ -288,9 +314,41 @@ The security value of a Type 4 app is in the **acquisition and caching** layers:
288
314
289
315
### Consumer mapping
290
316
291
-
| Consumer | Integration Type | Mechanism |
292
-
|---|---|---|
293
-
|`sshenc`| Type 1 (HelperTool) | SSH agent protocol; keys used in-process for signing |
294
-
|`awsenc`| Type 1 (HelperTool) |`credential_process` directive in `~/.aws/config`|
295
-
|`npmenc`| Type 2 (EnvInterpolation) |`.npmrc` with `${NPM_TOKEN}` placeholders |
296
-
|`sso-jwt`| Type 4 (CredentialSource) | Obtains and caches JWTs; consumed by Type 1/2/3 apps as a token provider |
|`sshenc`|`sshenc`, `sshenc-agent`, `sshenc-keygen`, `sshenc-pkcs11`, `gitenc`, `sshenc-tpm-bridge`| Type 1 (HelperTool) | SSH agent protocol; keys used in-process for signing |
320
+
|`awsenc`|`awsenc`, `awsenc-tpm-bridge`| Type 1 (HelperTool) |`credential_process` directive in `~/.aws/config`|
321
+
|`npmenc`|`npmenc`, `npxenc`| Type 2 (EnvInterpolation) |`.npmrc` with `${NPM_TOKEN}` placeholders |
322
+
|`sso-jwt`|`sso-jwt`, `sso-jwt-napi`, `sso-jwt-tpm-bridge`| Type 4 (CredentialSource) | Obtains and caches JWTs; consumed by Type 1/2/3 apps as a token provider |
323
+
324
+
## WSL bridge discovery
325
+
326
+
The WSL client (`enclaveapp-bridge::client::find_bridge`) searches a fixed list of `/mnt/c/` paths for the Windows bridge binary — PATH-based fallback via `which` was removed because a user-writable `$PATH` entry could substitute a malicious bridge, and the library performs no Authenticode verification on the resolved executable. Candidate paths:
327
+
328
+
```
329
+
/mnt/c/Program Files/<app>/<app>-tpm-bridge.exe
330
+
/mnt/c/ProgramData/<app>/<app>-tpm-bridge.exe
331
+
/mnt/c/Program Files/<app>/<app>-bridge.exe
332
+
/mnt/c/ProgramData/<app>/<app>-bridge.exe
333
+
```
334
+
335
+
Operators who install the bridge outside these locations must symlink into one of them. The bridge protocol enforces additional runtime bounds:
336
+
337
+
-`MAX_BRIDGE_RESPONSE_BYTES = 64 KB` — oversized responses are rejected.
338
+
-`DEFAULT_BRIDGE_REQUEST_TIMEOUT = 120 s` — covers Windows Hello prompts; override via `ENCLAVEAPP_BRIDGE_TIMEOUT_SECS`.
339
+
-`BRIDGE_SHUTDOWN_TIMEOUT = 5 s` after stdin close before the child is killed.
340
+
-`BridgeSession::Drop` kills and reaps the child — no zombie processes.
341
+
342
+
Authenticode / `WinVerifyTrust` verification on the resolved bridge binary is a tracked hardening gap for environments where the Windows host itself is semi-trusted.
343
+
344
+
## Credential cache file tamper
345
+
346
+
Credential caches are stored as `[header][AES-GCM ciphertext]` pairs on disk. The header (magic, version, flags, timestamps, risk level, optional session-expiration fields) is **not** authenticated by AAD — the `EncryptionStorage::encrypt` / `decrypt` trait does not currently accept associated data. A same-UID attacker with file-write access to the cache file can edit header fields without invalidating the ciphertext.
347
+
348
+
Consumer-layer mitigations already neutralize the practical risk-level-downgrade threat:
349
+
350
+
-**`max(header, config)` on read** — sso-jwt's `effective_cached_risk_level` (`sso-jwt-lib/src/cache.rs:57-59`) and awsenc's equivalent always clamp the effective risk level back up to the configured minimum. Editing the header down does nothing.
351
+
-**Server-side expiration is authoritative** — STS credentials (`awsenc`) carry `Expiration`; JWTs (`sso-jwt`) carry `exp`. Header-rolled timestamps don't extend server acceptance.
352
+
-**Payload-embedded timestamps** — both consumers recheck `session_start` / `token_iat` / `expiration`*after* decrypt, ignoring whatever the unencrypted header claims.
353
+
354
+
AAD binding the header to the ciphertext (a proper cryptographic fix) is deferred. It would require a trait signature change across all four backends (SE, CNG, keyring, test-software) plus every consumer, plus a one-time on-disk format migration. See `THREAT_MODEL.md` § "Credential cache header tamper" for the full rationale.
0 commit comments