diff --git a/Cargo.lock b/Cargo.lock index 91e69c2..3986e25 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -109,7 +109,7 @@ dependencies = [ [[package]] name = "alkssh" -version = "0.1.0" +version = "0.2.0" dependencies = [ "alkcall", "alkgit", @@ -131,9 +131,9 @@ dependencies = [ [[package]] name = "alktty" -version = "0.8.1" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2265167842b7d3ea09bc235f48450d3dcf22049a2ca71f8155de00acbf97c226" +checksum = "9cab9b0d3d6c1bdef2622799e3b95bb6063720153a124a57f4c60f95f3d42ea8" dependencies = [ "alkcall", "async-trait", @@ -153,9 +153,9 @@ dependencies = [ [[package]] name = "alktunnels" -version = "0.4.1" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee56da894f2d02120d3602df32c7f6a9db7f930a401df66d40b632cd9767af41" +checksum = "198d3aa766889cf5cc9b764319f9aa75e21234d623021893c26675f7c4276242" dependencies = [ "alkcall", "bytes", @@ -169,9 +169,9 @@ dependencies = [ [[package]] name = "alkvault" -version = "0.1.0" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52e032b074f7fea71ea15528994c5b3a6503c082c519045bb3b0519281291918" +checksum = "b2522ded10a506770779e72063422057bb63f1b75457c1742a3362072adfb1c4" dependencies = [ "aes-gcm", "base64", diff --git a/Cargo.toml b/Cargo.toml index 9e1c5dd..20945a0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,7 +8,7 @@ exclude = ["fuzz"] [package] name = "alkssh" -version = "0.1.0" +version = "0.2.0" edition = "2024" rust-version = "1.89" license = "MIT OR Apache-2.0" @@ -52,25 +52,37 @@ async-trait = "0.1" # russh keys path). Never enable russh's `des`/`dsa` features. russh = { version = "0.65", default-features = false, features = ["aws-lc-rs"] } alkcall = "0.10" -alkvault = "0.1" +# alkvault 0.2 (breaking req bump, 0.1 -> 0.2, 2026-11-04): `decrypt` +# returns `Zeroizing` (alkvault review 001's resolution — the +# pre-wrap plaintext window ADR-005 decision 3 recorded is closed +# upstream); the adapter-side wrap is dropped. Pin re-verified against +# crates.io: MIT OR Apache-2.0. +alkvault = "0.2" # Wave-3 seam-handle types (`config.rs`'s `SessionSeam`/`GitSeam`). -# Pins verified against crates.io (2026-10-28): alktty 0.8.1, alkgit -# 0.1.0 — both MIT OR Apache-2.0, both rust_version 1.88 ≤ our 1.89, -# both over alkcall ^0.10 (shared with our `alkcall = "0.10"` → 0.10.1 -# resolution). alktty's `local` feature stays off — alkssh never -# touches a real backend; the `local` shapes stay alktty-side. -alktty = { version = "0.8.1", default-features = false, optional = true } +# Pins verified against crates.io (2026-11-04): alktty 0.8.2 (0.1.0's +# 0.8.1 pin refreshed — 0.8.2 is the PTY exit-signal-fidelity fix, +# patch-level, API/wire unchanged; alkssh consumes negative exit +# codes verbatim so the derivation contract needed no change), alkgit +# 0.1.0 — both MIT OR Apache-2.0, both rust_version ≤ our 1.89, +# both over alkcall ^0.10 (shared with our `alkcall = "0.10"` → +# 0.10.1 resolution). alktty's `local` feature stays off — alkssh +# never touches a real backend; the `local` shapes stay alktty-side. +alktty = { version = "0.8.2", default-features = false, optional = true } alkgit = { version = "0.1.0", default-features = false, optional = true } # tunnel-only: the route-1 bridge's consumer surface (`TunnelSession:: -# open_direct`/`take_halves`, `SubstrateAddr`/`Substrate`). Pin -# re-verified against crates.io at `w3-tunnel-direct` (2026-10-29): -# 0.4.1 is `max_stable_version`/`newest_version`, MIT OR Apache-2.0, -# rust_version 1.88 ≤ our 1.89, alkcall ^0.10.1 pinned exactly -# (single-versioned against our `alkcall = "0.10"`). The `local` -# feature stays off — the dial seam is injected (`DialFn`); the real -# dial/backends belong to the assembly, and the tests hand-roll their -# own dial/dial-fail seams (`tests/tunnel_mapping.rs`). -alktunnels = { version = "0.4.1", default-features = false, optional = true } +# open_direct`/`take_halves`, `SubstrateAddr`/`Substrate`) + the +# forwarded serving surface's establisher shape +# (`register_tunnel_forwarded_openable_with_establisher` + +# `ForwardedGate` — the review-003 W2 adoption, ADR-008 Amendment 2's +# recommended shape). Pin re-verified against crates.io (2026-11-04): +# 0.5.0 is newest_version, additive API only over 0.4.1 (the wire +# delta is empty, the base registration is byte-for-byte), MIT OR +# Apache-2.0, rust_version 1.88 ≤ our 1.89, alkcall ^0.10.1 pinned +# exactly (single-versioned against our `alkcall = "0.10"`). The +# `local` feature stays off — the dial seam is injected (`DialFn`); +# the real dial/backends belong to the assembly, and the tests +# hand-roll their own dial/dial-fail seams (`tests/tunnel_mapping.rs`). +alktunnels = { version = "0.5.0", default-features = false, optional = true } # session-only: the OQ-44 reserved ttymodes JSON (`TerminalParamsWire.modes`) # and the exit-chunk / negotiation-error-frame JSON reads in `tty_map.rs`. # Same pin alktty carries (crate resolution stays single-versioned). @@ -103,8 +115,8 @@ serde_json = "1" # composition — the dev-dep `features = ["local"]` unification applies # to the TEST graph only (feature unification folds `local` into the # optional alktty dep in test builds; the published lib never enables -# it and `src/` never names a local shape). Same 0.8.1 pin. -alktty = { version = "0.8.1", default-features = false, features = ["local"] } +# it and `src/` never names a local shape). Same 0.8.2 pin. +alktty = { version = "0.8.2", default-features = false, features = ["local"] } [profile.release] lto = "thin" \ No newline at end of file diff --git a/fuzz/Cargo.lock b/fuzz/Cargo.lock index 008799e..cba926c 100644 --- a/fuzz/Cargo.lock +++ b/fuzz/Cargo.lock @@ -109,7 +109,7 @@ dependencies = [ [[package]] name = "alkssh" -version = "0.1.0" +version = "0.2.0" dependencies = [ "alkcall", "alktty", @@ -146,9 +146,9 @@ dependencies = [ [[package]] name = "alktty" -version = "0.8.1" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2265167842b7d3ea09bc235f48450d3dcf22049a2ca71f8155de00acbf97c226" +checksum = "9cab9b0d3d6c1bdef2622799e3b95bb6063720153a124a57f4c60f95f3d42ea8" dependencies = [ "alkcall", "async-trait", @@ -166,9 +166,9 @@ dependencies = [ [[package]] name = "alkvault" -version = "0.1.0" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52e032b074f7fea71ea15528994c5b3a6503c082c519045bb3b0519281291918" +checksum = "b2522ded10a506770779e72063422057bb63f1b75457c1742a3362072adfb1c4" dependencies = [ "aes-gcm", "base64", diff --git a/fuzz/shared/src/key_decode.rs b/fuzz/shared/src/key_decode.rs index d48735c..e1420d3 100644 --- a/fuzz/shared/src/key_decode.rs +++ b/fuzz/shared/src/key_decode.rs @@ -5,10 +5,11 @@ //! `decrypt_and_decode`'s exact decode stage, not a fork). //! //! Invariants: no-panic on arbitrary plaintext shapes (the production -//! plaintext is the vault's `String` — valid UTF-8 by return type; the -//! battery shapes arbitrary bytes into that form via the lossy -//! conversion and drives it through the same `Zeroizing` wrap the -//! adapter seam applies) and arbitrary passphrase shapes (the +//! plaintext is the vault's `Zeroizing` — valid UTF-8 by +//! return type; the battery shapes arbitrary bytes into that form via +//! the lossy conversion and drives it through the same zeroized +//! wrap the vault's `decrypt` return applies, alkssh 0.2.0 seam) and +//! arbitrary passphrase shapes (the //! underlying decode's passphrase arm — the production chain always //! passes `None`; the `Some` arm is robustness drive only); every //! failure is the typed [`KeySourceError::Decode`] class — never a @@ -16,7 +17,8 @@ //! failure text never carries the plaintext (the assertable half of //! the Zeroizing posture — the plaintext exists solely inside the //! call, dropped zeroized; no retention, no static cache — and the -//! typed error's only payload is the decode machinery's message). +//! typed error's only payload is the decode machinery's message +//! class, never the plaintext). //! //! The no-echo invariant's assertable form is *the bounded-message //! pair* (reshaped in-task after the first campaign's finding): the diff --git a/src/config.rs b/src/config.rs index 191a1e0..c935b66 100644 --- a/src/config.rs +++ b/src/config.rs @@ -202,7 +202,7 @@ impl Default for Limits { /// The `session` feature's alktty seam handle (`mapping.md` §The /// seams): what `drive_session[_pre_negotiated]` takes that the -/// assembly composes (alktty 0.8.1's `adapter.rs:205-241` signature). +/// assembly composes (alktty 0.8.2's `adapter.rs:205-241` signature). /// **Shape decision** (recorded at `w3-seam-config`): a whole-`Option` /// struct per seam, not flat `Option` fields — each seam is consumed /// atomically by exactly one seam call, so a struct admits no diff --git a/src/key_source.rs b/src/key_source.rs index 2d13b3d..1c62716 100644 --- a/src/key_source.rs +++ b/src/key_source.rs @@ -18,9 +18,7 @@ //! `key_version` interpretation, no record-internals access //! anywhere in the crate; version semantics belong to the vault. //! - **At-use lifecycle** (decision 2): `vault.decrypt(blob)` → -//! `Zeroizing` wrap at the adapter seam (decision 3 — the -//! known-gap contract; the vault-side improvement is upstream, not -//! here) → one-time +//! the at-use chain's single-use read → one-time //! [`decode_secret_key`](russh::keys::decode_secret_key) → the //! decoded `PrivateKey` lives for the dial's lifetime. The vault is //! needed only at use time: locking the vault mid-session keeps @@ -72,8 +70,8 @@ pub trait KeySource: Send + Sync + 'static { /// /// This is the only surface where the dial direction touches /// secret material: the plaintext exists solely inside this - /// method and is dropped (zeroized from the adapter seam onward) - /// before the decoded key is returned. + /// method and is zeroized by the vault's `Zeroizing` return + /// at its drop, before the decoded key is returned. async fn load(&self) -> Result; } @@ -171,21 +169,23 @@ impl HostKeySource for VaultHostKeySource { } } -/// The ADR-005 at-use chain (decision 2): one vault decrypt, the -/// `Zeroizing` seam wrap, one `decode_secret_key`. +/// The ADR-005 at-use chain (decision 2): one vault decrypt, one +/// `decode_secret_key`. /// -/// The plaintext `String` leaves the vault as -/// [`KeySourceError`]-mapped failure or enters the wrap immediately — -/// its only unfenced window is the vault's own `String` return, the -/// known-gap contract of ADR-005 decision 3 (the upstream ask is -/// alkvault's; OQ-07). The decode is the single inner-format -/// conversion (decision 1); a plaintext that is not OpenSSH-PEM key -/// material is a [`KeySourceError::Decode`] refusal. +/// The plaintext leaves the vault as +/// [`KeySourceError`]-mapped failure or as alkvault 0.2's +/// `Zeroizing` return (the pre-wrap unfenced window ADR-005 +/// decision 3 recorded is closed upstream — the vault fences the +/// plaintext from its return onward, zeroized on drop, including the +/// vault's own UTF-8 failure path; OQ-07's upstream ask is resolved, +/// amending the ADR's decision 3 note). The decode is the single +/// inner-format conversion (decision 1); a plaintext that is not +/// OpenSSH-PEM key material is a [`KeySourceError::Decode`] refusal. fn decrypt_and_decode( vault: &VaultServiceHandle, blob: &EncryptedData, ) -> Result { - let plaintext = Zeroizing::new(vault.decrypt(blob).map_err(map_vault_error)?); + let plaintext = vault.decrypt(blob).map_err(map_vault_error)?; decode_key_arm(&plaintext) } @@ -199,8 +199,9 @@ fn decrypt_and_decode( /// not a fork). Not documented API: the vault-facing surface is /// exactly [`VaultKeySource`] / [`VaultHostKeySource`]. /// -/// The `Zeroizing` posture rides the signature: the plaintext arrives -/// already wrapped at the adapter seam (decision 3), the decode reads +/// The `Zeroizing` posture rides the signature: the plaintext +/// arrives already zeroized (alkvault 0.2's return type — decision +/// 3's ownership moved upstream), the decode reads /// it once, and nothing is retained — `Ok` carries the decoded /// [`PrivateKey`], `Err` carries only the typed [`KeySourceError`] /// class (never the plaintext). diff --git a/tests/doubles.rs b/tests/doubles.rs index 052905c..2ad989e 100644 --- a/tests/doubles.rs +++ b/tests/doubles.rs @@ -117,8 +117,9 @@ fn fixture_keys_carry_ssh_keygen_recorded_fingerprints() { /// /// - *Contract stamp:* ADR-005 decision 1 (inner format: unencrypted /// OpenSSH-PEM in the record). -/// - *Contract stamp:* ADR-005 decision 2 (decrypt-at-use → -/// `Zeroizing` wrap → one-time `decode_secret_key`). +/// - *Contract stamp:* ADR-005 decision 2 (decrypt-at-use → the +/// vault's `Zeroizing` return → one-time +/// `decode_secret_key`). #[tokio::test] async fn test_vault_seal_then_key_source_decode_round_trips() { let alpha = fixture("alpha");