fix(key-source): decode failures carry a crate-owned message
The fuzz campaign (w5-fuzz-harness) found russh's ssh-key PPK parse path interpolating the raw header line — the caller's plaintext bytes — into its Display; passing that message through KeySourceError::Decode's payload could echo decrypted-record material into a stored/logged error string (ADR-005 decision 7's textual leak). decode_key_arm (the production decrypt-and-decode decode arm, exposed #[doc(hidden)] for the sidecar) now maps every decode failure to a crate-owned fixed message naming the refusal — the machinery's text never rides the typed payload. The upstream no-echo ask is recorded for russh/ssh-key. Verification: scripts/matrix.sh green; the fuzz sidecar's key_decode campaign re-ran clean (606 s, fork mode, zero crashes).
This commit is contained in:
1 parent
69d888a231
commit
a5fb03e7c6
2 files changed
+44
-4
No files matched your search
+6
-3
@@ -94,9 +94,12 @@ pub enum KeySourceError {
|
||||
/// private-key PEM (decision 1's inner format). Distinct from
|
||||
/// [`Decryption`](KeySourceError::Decryption) on purpose — the
|
||||
/// custody boundary held, what was guarded is not a key. The
|
||||
/// parse error's message is honest content; the typed parse
|
||||
/// error is deliberately not preserved so the RC-pinned
|
||||
/// `ssh-key` error type never enters the crate's public API.
|
||||
/// underlying parse error's message is deliberately *not*
|
||||
/// carried — the machinery's text can echo plaintext-shaped
|
||||
/// material (the `w5-fuzz-harness` finding: the `ssh-key` PPK
|
||||
/// path interpolates the raw header), so the payload is
|
||||
/// crate-owned fixed text; the RC-pinned `ssh-key` error type
|
||||
/// never enters the crate's public API either way.
|
||||
///
|
||||
/// Fail-closed consequence: no dial; on the host-key path, no
|
||||
/// server construction.
|
||||
|
||||
+38
-1
@@ -186,7 +186,44 @@ fn decrypt_and_decode(
|
||||
blob: &EncryptedData,
|
||||
) -> Result<PrivateKey, KeySourceError> {
|
||||
let plaintext = Zeroizing::new(vault.decrypt(blob).map_err(map_vault_error)?);
|
||||
decode_secret_key(&plaintext, None).map_err(|e| KeySourceError::Decode(e.to_string()))
|
||||
decode_key_arm(&plaintext)
|
||||
}
|
||||
|
||||
/// The at-use chain's decode arm — the one-time
|
||||
/// [`decode_secret_key`] conversion of [`decrypt_and_decode`]
|
||||
/// (ADR-005 decision 2's single conversion; this function is byte-for-
|
||||
/// byte that arm, extracted so the arm is the *only* shape the fuzz
|
||||
/// target drives). `#[doc(hidden)]` fuzz-visible surface
|
||||
/// (`w5-fuzz-harness`; the recorded exposure decision — the ingress is
|
||||
/// production-private, so the exposure is the production arm itself,
|
||||
/// 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
|
||||
/// it once, and nothing is retained — `Ok` carries the decoded
|
||||
/// [`PrivateKey`], `Err` carries only the typed [`KeySourceError`]
|
||||
/// class (never the plaintext).
|
||||
///
|
||||
/// The typed `Decode` payload is crate-owned fixed text, deliberately
|
||||
/// *not* the underlying machinery's message (the `w5-fuzz-harness`
|
||||
/// finding, 2026-10-30): russh's/`ssh-key`'s PPK parse path
|
||||
/// (`PpkParseError::Header`) interpolates raw header bytes — the
|
||||
/// plaintext's own material — into its Display, so a machinery message
|
||||
/// riding the typed payload would echo decrypted-record material
|
||||
/// exactly the no-persist posture (decision 7) forbids. The upstream
|
||||
/// echo ask is recorded with the task; if the machinery gains an
|
||||
/// echo-free message taxonomy, the mapping can carry its granularity.
|
||||
#[doc(hidden)]
|
||||
pub fn decode_key_arm(plaintext: &Zeroizing<String>) -> Result<PrivateKey, KeySourceError> {
|
||||
decode_secret_key(plaintext, None).map_err(|_| {
|
||||
KeySourceError::Decode(
|
||||
"the plaintext is not OpenSSH private-key PEM material — the decode machinery \
|
||||
refused it (the machinery's message is not carried: it can echo plaintext \
|
||||
material; the upstream echo ask is recorded)"
|
||||
.to_owned(),
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
/// The vault's error taxonomy reshaped into ADR-005 decision 4's
|
||||
|
||||
Reference in new issue
Block a user