security: add secret-hardening suite (pinning, rate limits, TTL, rekey, recovery)#69
Merged
Conversation
allowed_callers entries may carry an optional @sha256=<hex> suffix. When present the service hashes the caller's binary at connect time and rejects the connection on mismatch, closing the swap-binary-at-approved-path gap in the path-only allowlist. `cr policy allow PATH --pin` records the pin. - crypto/binhash.zig: sha256Hex, hashFileHex, constant-time hexEql - policy: parseCaller, callerExpectedHash, path-aware isCallerAllowed - service: fail-closed callerHashOk gate on task_declare/spawn
The success path logged secret_injected and task_end but never named the binary that actually ran; the resolved absolute target only appeared on rejection (target_rejected). task_spawned records the resolved argv[0] and child pid once the child exists, on both POSIX and Windows spawn paths.
Adds max_spawns/window_ms to Task. The service tracks a fixed-window spawn counter per task and refuses spawns beyond the cap, bounding how many times a trusted caller can pull a task's secrets into a subprocess. 0 = unlimited (default, backward compatible). - service/ratelimit.zig: pure fixed-window Window.allow (injected clock) - policy: max_spawns/window_ms fields + roundtrip - service: per-task window map, fail-closed gate after the target check - audit: rate_limited event - cli: --max-spawns/--window-ms on `policy task add`, shown in policy show
The service socket was /tmp/cora-<uid>.sock — one per user, so two projects could not be unlocked at once. POSIX now folds a hash of the working directory into the name (/tmp/cora-<uid>-<cwdhash>.sock); same-cwd clients recompute the identical path. CORA_SOCK overrides it explicitly. Windows named-pipe naming is unchanged. - service.formatPosixSocket: pure, per-cwd, tested - defaultSocketPath: CORA_SOCK env override, cwd-derived default
Import secret values straight from the operator's environment instead of typing them on argv (which leaks into shell history and ps). Names whose variables are unset are reported and skipped; a partial import still writes what it found. Integration test spawns cr with a one-entry environment and asserts set/unset handling and that the value never reaches stdout/stderr.
Emits a systemd --user unit (Linux) or launchd LaunchAgent plist (macOS) running `cr unlock --foreground` from the current directory. No auto-start / RunAtLoad / Restart: the passphrase model requires interactive unlock, so the template supervises but never brings the service up unattended. - service/daemon.zig: pure renderSystemdUnit / renderLaunchdPlist (tested) - cli: `cr daemon unit`, resolves own path via pid lookup + cwd
Decrypts with the current passphrase and re-encrypts secrets + policy under a freshly derived key (new random salt/nonce), so a leaked old passphrase can no longer open the file. Secret values live in memory only for the call and are zeroed on return. Integration test covers old-fails/new-works and mismatch rejection.
Secrets can carry expiry (and creation) metadata. The service refuses to
inject an expired secret — it is treated as missing and audited as such — so
a stale credential never reaches a child process. Backward compatible: the
codec reads and writes flat {"K":"v"} for metadata-free secrets and only
emits the rich {"v":..,"exp":..,"cr":..} form when a TTL is set.
- store/mem.zig: Entry{secret,expires_ms,created_ms}, isExpired, putWithMeta
- store/secrets_codec.zig: parse/emit both flat and rich forms
- service: injection gated on entry.isExpired(now)
- cli: `secrets set KEY --ttl SECONDS`
`recovery backup` seals an independent encrypted copy (cora.zon.recovery) under a separate passphrase; `recovery restore` rebuilds the vault from it under a new primary passphrase, regaining access when the primary is lost. Implemented as a point-in-time encrypted snapshot reusing createEncrypted / decrypt, deliberately leaving the on-disk format and save path untouched. A live second key slot would need envelope encryption and a master-key save refactor across every mutation path; the snapshot trades manual refresh (re-run backup after changing secrets) for that risk. Integration test covers backup -> restore -> new passphrase works, old passphrase rejected.
std.c.getpid() returns an opaque handle on Windows, breaking the Windows build. Resolve the pid via GetCurrentProcessId there. Verified by cross- compiling the exe for x86_64-windows, x86_64-linux-gnu, and aarch64-macos.
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
Security-hardening suite: nine gaps identified in the pre-alpha runtime, each implemented TDD-first with the full suite kept green after every change. All work is additive and backward-compatible with existing
cora.zonfiles.@sha256=<hex>pins inallowed_callers; service hashes the caller binary at connect and rejects mismatch.cr policy allow PATH --pincr rekey— decrypt + re-encrypt under a freshly derived keyTask.max_spawns/window_ms; fixed-window limiter, fail-closed gate/tmp/cora-<uid>-<cwdhash>.sock+CORA_SOCKoverridecr secrets set KEY --ttl Scr secrets import --from-env NAME...(values never on argv)cr daemon unitprints a systemd/launchd templatetask_spawnedevent records the resolved argv[0] + pidDesign notes
cr tuiwith pane-based vaxis UI #9 recovery is a point-in-time encrypted copy (cora.zon.recovery), not a live key slot. A live second slot needs envelope encryption plus a master-key refactor of every save path; the snapshot delivers break-glass recovery while leaving the on-disk format and save path untouched. Re-runbackupafter changing secrets.import,rekey,recovery,--ttl) have integration tests against the realcrbinary.Verification
zig build fmt,zig build -Doptimize=Debug,zig build testall green locally (132/134, 2 pre-existing skips).std.c.getpid()returns an opaque handle on Windows).