Skip to content

Feature Request: Expose SSH KEX shared secret for protocol bridging use cases #603

@stevenparkerco3

Description

@stevenparkerco3

GitHub Issue: Request to Expose KEX Shared Secret

Summary

We're implementing a Rust client that uses SSH for authentication and session establishment, but needs to derive encryption keys for a separate data channel (UDP-based). This pattern (using SSH KEX to bootstrap keys for a secondary protocol) requires access to the raw shared secret, which russh currently does not expose.

Use Case: Protocol Bridging

Several protocols use SSH as an authentication/session layer but transfer data over separate channels:

  • High-speed file transfer protocols that use UDP for throughput
  • VPN-over-SSH implementations with separate data planes
  • Multimedia streaming protocols using SSH for signaling
  • Custom enterprise protocols using SSH for auth but proprietary transports

The common pattern is:

1. Establish SSH connection (TCP)
2. Perform SSH KEX → obtain shared secret K
3. Derive secondary keys: HKDF(K, label, context) → encryption keys for other channel
4. Use derived keys for UDP/other transport encryption

Without access to the raw shared secret (K), step 3 is impossible, even though the SSH connection succeeds.

Current Behavior

The shared secret is computed in KexAlgorithm::compute_shared_secret() and stored privately:

// kex/curve25519.rs
pub struct Curve25519Kex {
    local_secret: Option<Scalar>,
    shared_secret: Option<MontgomeryPoint>,  // ← Private, no accessor
}

It's consumed internally by compute_keys() to derive SSH encryption keys, but never exposed to the library user.

Proposed Solution

Add an optional method to retrieve the shared secret bytes. Three possible approaches:

Option A: Method on KexAlgorithm trait

pub trait KexAlgorithm {
    // ... existing methods ...
    
    /// Get the raw shared secret bytes (if available)
    /// 
    /// Returns None if KEX hasn't completed or secret was already consumed.
    /// 
    /// # Security Note
    /// This exposes sensitive cryptographic material. Only use for protocols
    /// that require deriving additional keys from the SSH shared secret.
    fn shared_secret_bytes(&self) -> Option<&[u8]>;
}

Option B: Handler callback after KEX

#[async_trait]
pub trait Handler: Sized + Send {
    // ... existing methods ...
    
    /// Called after successful key exchange with the shared secret.
    /// 
    /// Default implementation discards the secret. Override to capture it
    /// for protocols that derive additional keys from SSH KEX.
    #[allow(unused_variables)]
    async fn kex_completed(
        &mut self,
        kex_algorithm: &str,
        shared_secret: &[u8],
        session: &mut Session,
    ) -> Result<(), Self::Error> {
        Ok(())
    }
}

Option C: Session method (most conservative)

impl Session {
    /// Get the raw shared secret (K) from the most recent KEX.
    /// 
    /// # Security Warning
    /// Exposes sensitive cryptographic material. Only use for protocols
    /// requiring key derivation from SSH KEX (e.g., protocol bridging).
    pub fn kex_shared_secret(&self) -> Option<&[u8]>;
}

Why Not Fork?

We considered forking russh, but:

  1. Security maintenance burden - SSH libraries require ongoing security attention
  2. Tracking upstream - We'd need to continuously merge security fixes
  3. Community benefit - Other protocol bridging use cases would benefit from upstream support

Alternatives Considered

Alternative Why Not Viable
Fork russh Security maintenance burden
Use ssh2 (libssh2) Also doesn't expose shared secret
Implement SSH KEX ourselves Duplicates security-critical code
Intercept at network level Fragile, defeats purpose of using SSH library

Security Considerations

We understand the hesitation to expose cryptographic secrets. Some mitigations:

  1. Opt-in only - Default implementation returns None
  2. Clear documentation - Warn about security implications
  3. Feature flag - Gate behind expose-kex-secret feature (optional)
  4. Handler callback - Makes access explicit and auditable

The shared secret is already available to anyone who implements SSH themselves. Exposing it through a controlled API is arguably safer than forcing users to fork or reimplement.

Minimal Implementation

If helpful, here's a minimal diff for Option A (curve25519 only):

// kex/mod.rs
pub trait KexAlgorithm {
    fn skip_exchange(&self) -> bool;
    fn server_dh(&mut self, exchange: &mut Exchange, payload: &[u8]) -> Result<(), Error>;
    fn client_dh(&mut self, client_ephemeral: &mut CryptoVec, buf: &mut CryptoVec) -> Result<(), Error>;
    fn compute_shared_secret(&mut self, remote_pubkey: &[u8]) -> Result<(), Error>;
    fn compute_exchange_hash(&self, key: &CryptoVec, exchange: &Exchange, buffer: &mut CryptoVec) -> Result<CryptoVec, Error>;
    fn compute_keys(&self, ...) -> Result<CipherPair, Error>;
+   
+   /// Get raw shared secret bytes for protocol bridging use cases.
+   fn shared_secret_bytes(&self) -> Option<&[u8]> {
+       None  // Default: not available
+   }
}

// kex/curve25519.rs
impl KexAlgorithm for Curve25519Kex {
    // ... existing methods ...
    
+   fn shared_secret_bytes(&self) -> Option<&[u8]> {
+       self.shared_secret.as_ref().map(|s| s.0.as_slice())
+   }
}

Similar changes would be needed for other KEX algorithms (ECDH, DH groups).

Questions for Maintainers

  1. Is there a security concern with exposing the shared secret that we're missing?
  2. Would you prefer the Handler callback approach to make access more explicit?
  3. Would a feature flag (expose-kex-secret) be acceptable to gate this functionality?
  4. Is there an existing pattern in russh for this kind of "escape hatch" functionality?

We're happy to contribute a PR if you're open to any of these approaches.


Suggested Labels

  • enhancement
  • feature-request
  • api

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions