Repository navigation
fix(stack-auth,stack-profile): clamp server expires_in and wipe secret buffers - #1553
Draft
cipherstash-agent wants to merge 3 commits into
Draft
cipherstash-agent wants to merge 3 commits into
cipherstash-agent wants to merge 3 commits into
Conversation
…t buffers A server-supplied `expires_in` was added to the clock unchecked. On the token paths `now + expires_in` panics in debug and wraps in release (no overflow-checks), giving an already-expired token and a refresh loop; the device-code deadline `Instant::now() + Duration` panics in every profile. Token lifetimes now go through one helper, `expires_at_after`, which clamps to 30 days and saturates, the rule `is_expired_at` already followed. Device codes are clamped to one hour when received, so the deadline cannot overflow and `PendingDeviceCode::expires_in()` reports what is enforced. The JSON that carries `auth.json` and `secretkey.json` to and from disk was held in plain Strings and freed unwiped, undercutting `ZeroizeOnDrop` on the secrets inside. It is now serialised into a writer that wipes on drop and on every growth (a Zeroizing<Vec> alone frees its old allocation unwiped when it reallocates), and read through `fs::read`, which sizes the buffer once, into a Zeroizing<Vec<u8>>. The access key from `CS_CLIENT_ACCESS_KEY` or `with_access_key` is held as Zeroizing<String> until it is parsed into an `AccessKey`. Fixes CIP-5159. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012Jmn7qCTmGiT6hfLv8rxDP
…wiping - Read profile files through `SecretBuf::read_from` rather than `fs::read`. `fs::read` only avoids reallocating when the file's metadata length is right; on a filesystem that under-reports it, the Vec grows and frees unwiped copies, and the final probe read goes through an unwiped stack buffer. `SecretBuf` (renamed from `ZeroizingWriter`) reads straight into its own wiped buffer. - Hold the builder's access key as a `SecretToken`, the crate's existing secret type, rather than a bare `Zeroizing<String>`, so it is also masked in `Debug`. - Log the enforced device-code lifetime next to the server's, so the trace matches what polling does. - Drop a test that called `reserve` itself and so could not catch `write` skipping it; add `read_from` tests for short, interrupted and failed reads. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012Jmn7qCTmGiT6hfLv8rxDP
🦋 Changeset detectedLatest commit: dd043af The changes in this PR will be included in the next version bump. This PR includes changesets to release 19 packages
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
Contributor
|
Important Draft PR not reviewedDraft PRs are not automatically reviewed by default.
To automatically review draft PRs, update your CodeRabbit configuration: reviews:
auto_review:
drafts: true |
Contributor
Mutation testing (cargo-mutants,
|
| caught | missed | unviable | timeout |
|---|---|---|---|
| 10 | 0 | 5 | 0 |
Every mutant in the changed lines was caught by a test.
…ckfiles stack-profile gained a `zeroize` dependency, and six detached Cargo workspaces resolve it by path: both Go guests, EQL and the three fuzz crates. The Go guest job checks its lockfiles with --locked and failed on the stale entry. Each lockfile gains only the one dependency line. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012Jmn7qCTmGiT6hfLv8rxDP
This branch has not been deployed
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.
Requested by CipherStash · project thread
Summary
Fixes CIP-5159 (R3 and R4 from the weekly review).
expires_inarithmetic. The auth server'sexpires_inwas added to the clock with no limit. On the token paths,now + expires_inpanics in debug builds and wraps in release builds, because the release profile has nooverflow-checks. A wrapped value gives a token that is already expired, which causes a refresh loop. The device-code deadline,Instant::now() + Duration::from_secs(expires_in), panics in every build profile.Strings that are never wiped. The JSON forauth.jsonandsecretkey.jsonpassed throughto_string_prettyandread_to_stringbuffers that were freed without being wiped.CS_CLIENT_ACCESS_KEYwas held as a bareStringuntil it was parsed.Changes
token::expires_at_after(now, expires_in)is now the single place where a serverexpires_inbecomesexpires_at. It caps the lifetime at 30 days (MAX_TOKEN_LIFETIME_SECS) and uses a saturating add, the same ruleis_expired_atalready follows. BothToken::refreshand device-code polling use it.MAX_DEVICE_CODE_LIFETIME_SECS) when the code is received, so the poll deadline cannot overflow.PendingDeviceCode::expires_in()reports the lifetime that is actually enforced. The debug trace logs both that value and the server's value.stack-profilehas a new privateSecretBuf. It is aZeroizing<Vec<u8>>that never lets theVecreallocate itself. When it needs more room, it copies into a larger buffer and wipes the old one.ProfileStore::writeserialises into it withserde_json::to_writer_pretty.ProfileStore::loadreads the file straight into it withSecretBuf::read_fromand parses withfrom_slice.fs::readwas not used because it reallocates, and leaves unwiped copies, whenever file metadata under-reports the file's length.AutoStrategyBuilderholds the access key as aSecretToken, which wipes on drop and masksDebug. It wraps the key as soon as it reads it from the environment.detect_innernow borrows the key asOption<&str>.@cipherstash/authpatch changeset.Verification
cargo test -p stack-auth -p stack-profile --all-features: all pass. These are the stack-auth lib tests (297), stack-profile (67), and the integration and doc tests.Instantadd.cargo clippy --all-targets -D warningspasses with all features and with no default features. Clippy also passes for stack-profile onwasm32-wasip1(lib).cargo fmt --checkandcargo docare clean.scripts/check-auth-npm-changeset.mjslocally because there are nonode_modules. The changeset follows the format of the existing@cipherstash/authones.Review notes
/api/authorisereturns an absoluteexpiry(authorize_dto.rs). It involves no arithmetic, so it cannot overflow. It is still unbounded, though: a hostileexpiry: u64::MAXwould stopAutoRefreshfrom ever refreshing. Capping it means passingnowinto theFrom<AuthoriseResponse>mapping, which touches the refreshers that CIP-5152 and CIP-5158 are also changing, so I left it out of this PR.loadsays so.CS_CLIENT_ACCESS_KEY. That copy is outside the crate's reach.auto_strategy.rsis also touched by the CIP-5158 thread (CS_WORKSPACE_CRN). This PR only changes the access-key lines and drops.into()from fourdetect_innertest calls.Related
ServiceTokenzeroize) covers a different type.Generated by Claude Code