Repository navigation
Sealed memory: SealedFixed, SealedDynamic, locked Prekey (#56) - #265
Merged
Merged
Conversation
…S1) New `sealed` feature. Secrets are held as ChaCha20-Poly1305 ciphertext and opened only for one closure call. Each access derives the sealing key afresh as SHA-256(salt || 16 KiB pre-key); the key is never stored. The pre-key is an Arc-owned object, not a static: page-aligned, locked with region::lock (a failed lock is reported through lock_status, never fatal), and wiped before it is unlocked and freed. Also adds scoped::RevealScoped (kept out of the crate root) and sealed_fixed_newtype! for sealing fixed_newtype! roles. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Jd6eJgv7urGn4rvMcVASo2
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Jd6eJgv7urGn4rvMcVASo2
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Jd6eJgv7urGn4rvMcVASo2
…56 S2) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Jd6eJgv7urGn4rvMcVASo2
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Jd6eJgv7urGn4rvMcVASo2
… S4) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Jd6eJgv7urGn4rvMcVASo2
…ges larger than the pre-key (#56) From the independent audit. The ChaCha20 backend leaves its state block (constant, then the sealing key) on the stack, and release builds left one copy of the plaintext; an inline(never) zeroizing frame laid over that region after each seal and open removes both. On a page larger than 16 KiB the pre-key would share a page, so it is not locked and lock_status says why. SealedDynamic::seal holds its plaintext copy in a wiping buffer until it is ciphertext. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Jd6eJgv7urGn4rvMcVASo2
…he residue probe (#56) The probe from the audit found the plaintext once more in release after the first fix: the temporary was moved into the caller's frame, above the scrub. It is now built as zeros and decrypted in place inside an inline(never) frame. Before/after runs and the probe are in docs/design/receipts/sealed-stack-residue/. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Jd6eJgv7urGn4rvMcVASo2
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Jd6eJgv7urGn4rvMcVASo2
…t a literal (#56) with_role hands the closure the fixed_newtype! role itself, for code that takes &AppKey rather than bytes, opened and wrapped inside the scrubbed frame. Asked for by EFV, whose key consumers take the role. fresh_salt and two unit tests no longer start from a literal array, which CodeQL read as a hard-coded salt. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Jd6eJgv7urGn4rvMcVASo2
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Jd6eJgv7urGn4rvMcVASo2
Routing the salt through Fixed::from_random().into_inner() let CodeQL trace every Fixed::new([literal]) in tests and fuzz into a salt sink: 20 alerts in files this PR does not touch. Built from SysRng words with array::from_fn instead. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Jd6eJgv7urGn4rvMcVASo2
CodeQL's closure-capture flow joined every Fixed::new([literal]) in tests and fuzz to the &self.salt read inside open()'s closure: 20 false 'hard-coded salt' alerts. SealedFixed's open_into shape is not flagged; SealedDynamic now matches. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Jd6eJgv7urGn4rvMcVASo2
…and the secrecy migration note (#56) Requested by the owner via EFV COORDINATOR. Cites iqlusioninc/crates#1140, where secrecy 0.10 dropped its stack Secret<T> for the same reason. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Jd6eJgv7urGn4rvMcVASo2
…ence; unlink sealed types in the feature table (#56) The crate-root feature table linked SealedFixed / SealedDynamic / Prekey, which do not exist under --features=full, so cargo doc --features=full failed again (the break rc.13 already fixed once). CI documents --all-features only. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Jd6eJgv7urGn4rvMcVASo2
- Scrub on every exit: accesses run through scrubbed(), which catches an unwind (a panicking closure or a tag mismatch), scrubs, and resumes it. Panic paths left the sealing key on the stack before. - unseal opens in place in the caller's frame; the by-value return can still leave a copy there, the SECURITY.md section 1 hazard, now documented on unseal and the sealed role's unseal with a pointer to with_secret / with_role. - Lock the pre-key while it holds zeros, then fill it. - The page-size refusal is lock_block(), and its test calls it (deleting the condition used to leave every test green). - Sealed types are no longer Clone under cloneable (owner decision), with a compile_fail doctest. - Unit tests that lock pages share a quota guard, so the quota test has the quota to itself; lock tests accept refusals that the code reports by design. - String unseal wipes non-UTF-8 bytes before its unreachable panic. - Docs: panic paths, unseal, debug vs release probe counts, stack headroom, tag checked before decryption, is_empty; second probe and runs in receipts. - Regenerate fixed_zero_size.stderr for the line shift from 5805536. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Jd6eJgv7urGn4rvMcVASo2
…to (#56) Suggested by the review: under --all-features, sealed adds a second E0509 to newtype_manual_drop, an expected mismatch rather than a regression. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Jd6eJgv7urGn4rvMcVASo2
… review pass (#56) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Jd6eJgv7urGn4rvMcVASo2
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.
Builds the sealed-memory family tracked in #56, following
docs/design/sealed_memory.md(sequoia-openpgp'smem::Encryptedmodel), plus the additions EFV asked for: pre-key page locking, a soft lock failure, a wipeable owner object instead of a static, andsealed_fixed_newtype!.Kept open until EFV has tested it against the branch. Merge and publish follow EFV's sign-off; rc.13 is prepared but not tagged.
Release order (owner decision, 2026-10-18, relayed by EFV COORDINATOR)
All six publish together once EFV's #59 integration test is green:
=0.9.0-rc.13: aescrypt-rs, msoffice-crypto, odf-crypto, age-pq-hpke, age-pq-keysNot only a version bump for the siblings: rc.13 rejects
derive: [ConstantTimeEq]on byte-shapedfixed_newtype!arms, where it now comes for free. Checked by EFV COORDINATOR against the copies in EFV's lockfile: only age-pq-hpke uses it, four times insrc/aliases.rs(lines 39, 59, 65, 92 at age-pq-workspace 4e77123; E0119 against rc.13). Its re-pin release drops those fourderive:lists. age-pq-keys, aescrypt-rs, msoffice-crypto and odf-crypto change only the pin.EFV tests against a local, never-pushed port of this PR onto
v0.9.0-rc.12(owner's choice), because the siblings pin=0.9.0-rc.12.Shape
sealed = ["std", "rand", chacha20poly1305 0.11, sha2 0.11, region 4.0.1], not part offull. Default deps unchanged.Prekey: 16 KiB, aligned to 16 KiB, locked withregion::lock, held asArc<Prekey>by every sealed object. Wiped while still locked, then unlocked, then freed when the last reference drops.lock_status()reports a refused lock (LockError, OS code only), never panics.SHA-256(salt ‖ pre-key)recomputed per access, never stored; ChaCha20-Poly1305, detached tag, zero nonce, fresh salt on every seal and re-seal.SealedFixed<N>: inline ciphertext, opens into a stackFixed; no allocation per access.SealedDynamic<Vec<u8> | String>:with_secret_mutgives&mut [u8]/&mut str, so it cannot grow.scoped::RevealScoped(not at the root; glob-import hazard checked intests/sealed_glob_import.rs).sealed_fixed_newtype!(pub SealedAppKey, AppKey, "doc"): seals/unseals its own role only;with_role/with_role_muthand the closure&AppKey(compile_fail doctests).expose_secret, noRevealSecret, noHash;Debugis[REDACTED].Slices
SealedFixed<32>vsFixed<[u8; 32]>, SHA-256 vs BLAKE3)Deferred to the end pass
-D warningson stable for every CI feature row, plus--all-features--features=full,--all-features; trybuild compile_fail suite on 1.85 (noCARGO_TARGET_DIR)cargo +1.85 check --workspace --all-features --all-targetsaftercargo +1.85 updatecargo doc --all-featureswithRUSTDOCFLAGS=-D warnings, stable and nightly--cfg docsrscargo audit/ dependency tree: new deps, no duplicaterand_corescrub_stack(), a 32 KiB zeroizing#[inline(never)]frame (sequoia'szero_stack, in safe Rust).SealedFixedopens in place inside an#[inline(never)]frame below the scrub.docs/design/receipts/sealed-stack-residue/. x86_64 Windows only.lock_status()reportspage_too_large(). Traced, not run (no 64 KiB-page machine).Zeroizinguntil encrypted.Gate run (local,
RUSTFLAGS=-D warnings, stable 1.98.1 / 1.85 / nightly): 39 of 39 steps green: fmt; clippy and doctests over 7 feature rows; tests over 13 feature rows plus releasefulland release--all-features; trybuild compile_fail on 1.85; rustdoc stable and nightly--cfg docsrs; thumbv7em ×4; residue-probe. Sealed binaries confirmed run in the--all-featuresand--no-default-features --features=sealedrows. MSRV:cargo +1.85 updatethen check default / full /--workspace --all-features --all-targets, clean.cargo deny check advisoriesok;cargo tree -don normal deps empty.Release checks, read-only, done at f798236: crates.io index has no 0.9.0-rc.13; manifest = heading; no v0.9.0-rc.13 tag local or remote;
cargo publish --locked --dry-rungreen (47 files); the tarball also builds withsealed,--all-features(stable and 1.85) andfull, and documents with-D warnings.Release steps (owner-approved 2026-10-19; run when EFV signs off on #59)
main.main, the cut:## [0.9.0-rc.13] - Unreleased→ the ISO date. Nothing else.cargo publish --locked --dry-runon that commit; must be green.git tag -a v0.9.0-rc.13 -m "0.9.0-rc.13"on the cut commit, thengit push --follow-tags origin main(annotated tag only; never--tags). Checked: every local annotated tag is already on origin, so only the new one travels.secure-gate-sealed-memory,secure-gate-sealed-rc12-test).Resume here: waiting on EFV's sign-off (NAKED SECRETS rebuilding #1331 against the test copy at 2e5418e).
🤖 Generated with Claude Code
https://claude.ai/code/session_01Jd6eJgv7urGn4rvMcVASo2