puressh 0.2.3

A pure-Rust SSH (Secure Shell) protocol library, in the spirit of libssh, built on purecrypto.
Documentation
//! Algorithm negotiation (RFC 4253 §7) — the `SSH_MSG_KEXINIT` exchange and
//! the KEX runner that drives the round-trip to completion.

extern crate alloc;

/// Algorithm preference lists advertised by one side.
#[derive(Debug, Clone)]
#[non_exhaustive]
pub struct KexAlgorithms<'a> {
    /// Key exchange algorithms (e.g. `curve25519-sha256`).
    pub kex: &'a [&'a str],
    /// Host key / signature algorithms (e.g. `ssh-ed25519`, `rsa-sha2-512`).
    pub server_host_key: &'a [&'a str],
    /// Ciphers, client→server.
    pub ciphers_c2s: &'a [&'a str],
    /// Ciphers, server→client.
    pub ciphers_s2c: &'a [&'a str],
    /// MACs, client→server.
    pub macs_c2s: &'a [&'a str],
    /// MACs, server→client.
    pub macs_s2c: &'a [&'a str],
    /// Compression, client→server.
    pub comp_c2s: &'a [&'a str],
    /// Compression, server→client.
    pub comp_s2c: &'a [&'a str],
    /// Languages, client→server (usually empty).
    pub lang_c2s: &'a [&'a str],
    /// Languages, server→client (usually empty).
    pub lang_s2c: &'a [&'a str],
}

impl Default for KexAlgorithms<'_> {
    /// Every list empty. Assign the lists you care about afterwards; an
    /// empty list is never negotiable, so a default instance fails
    /// negotiation rather than silently picking something.
    fn default() -> Self {
        Self {
            kex: &[],
            server_host_key: &[],
            ciphers_c2s: &[],
            ciphers_s2c: &[],
            macs_c2s: &[],
            macs_s2c: &[],
            comp_c2s: &[],
            comp_s2c: &[],
            lang_c2s: &[],
            lang_s2c: &[],
        }
    }
}

/// The algorithms agreed by both sides after the KEXINIT exchange.
///
/// Carries owned strings so the runner can keep it across the rest of the
/// KEX round-trip and across re-keys.
#[derive(Debug, Clone)]
#[non_exhaustive]
pub struct Negotiated {
    /// Key-exchange method.
    pub kex: alloc::string::String,
    /// Server host-key method.
    pub host_key: alloc::string::String,
    /// Client to server cipher.
    pub cipher_c2s: alloc::string::String,
    /// Server to client cipher.
    pub cipher_s2c: alloc::string::String,
    /// Client to server MAC (or "" when cipher is AEAD).
    pub mac_c2s: alloc::string::String,
    /// Server to client MAC (or "" when cipher is AEAD).
    pub mac_s2c: alloc::string::String,
    /// Client to server compression.
    pub comp_c2s: alloc::string::String,
    /// Server to client compression.
    pub comp_s2c: alloc::string::String,
    /// For a group exchange the client ran: what it asked for and got.
    pub gex: Option<GexSizes>,
}

/// The sizes of a Diffie-Hellman group exchange, as OpenSSH's client logs
/// them (`SSH2_MSG_KEX_DH_GEX_REQUEST(min<n<max) sent`, `bits set: s/p`).
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub struct GexSizes {
    /// Smallest group asked for, in bits.
    pub min: u32,
    /// Preferred group size asked for.
    pub n: u32,
    /// Largest group asked for.
    pub max: u32,
    /// Size of the group the server chose.
    pub group_bits: u32,
    /// Bits set in the client's public value.
    pub bits_set: u32,
}

/// `kex-strict-c-v00@openssh.com` — sentinel name advertised by the client
/// to indicate it supports strict-kex (Terrapin / CVE-2023-48795).
/// See OpenSSH `PROTOCOL` and commit `1edb00c58`.
pub const STRICT_KEX_CLIENT_MARKER: &str = "kex-strict-c-v00@openssh.com";
/// `kex-strict-s-v00@openssh.com` — server-direction strict-kex marker.
pub const STRICT_KEX_SERVER_MARKER: &str = "kex-strict-s-v00@openssh.com";

/// Returns `true` if the given KEX algorithm name is one of the strict-kex
/// signalling markers (`kex-strict-{c,s}-v00@openssh.com`) rather than a
/// real KEX algorithm. The negotiator skips these when picking the agreed
/// kex but inspects them separately to set the strict-kex flag.
pub fn is_strict_kex_marker(name: &str) -> bool {
    name == STRICT_KEX_CLIENT_MARKER || name == STRICT_KEX_SERVER_MARKER
}

/// Sensible default algorithm lists matching modern OpenSSH.
pub mod defaults {
    use super::{STRICT_KEX_CLIENT_MARKER, STRICT_KEX_SERVER_MARKER};

    /// KEX algorithms in preference order. The trailing two entries
    /// (`kex-strict-{c,s}-v00@openssh.com`) are *signalling* names rather
    /// than negotiable algorithms — they tell the peer this side supports
    /// strict-kex (Terrapin / CVE-2023-48795). They are intentionally last
    /// so they cannot win the negotiation; the
    /// [`crate::transport::kexinit::negotiate`] picker also skips them when
    /// choosing a real KEX.
    pub const KEX: &[&str] = &[
        "mlkem768x25519-sha256",
        "sntrup761x25519-sha512",
        "sntrup761x25519-sha512@openssh.com",
        "curve25519-sha256",
        "curve25519-sha256@libssh.org",
        "ecdh-sha2-nistp256",
        "ecdh-sha2-nistp384",
        "ecdh-sha2-nistp521",
        "diffie-hellman-group-exchange-sha256",
        "diffie-hellman-group16-sha512",
        "diffie-hellman-group18-sha512",
        "diffie-hellman-group14-sha256",
        STRICT_KEX_CLIENT_MARKER,
        STRICT_KEX_SERVER_MARKER,
    ];

    /// Server host-key algorithms in preference order.
    pub const HOST_KEY: &[&str] = &[
        "ssh-ed25519",
        "ecdsa-sha2-nistp256",
        "ecdsa-sha2-nistp384",
        "ecdsa-sha2-nistp521",
        "rsa-sha2-512",
        "rsa-sha2-256",
    ];

    /// Cipher algorithms (same list for both directions).
    pub const CIPHERS: &[&str] = &[
        "chacha20-poly1305@openssh.com",
        "aes128-gcm@openssh.com",
        "aes256-gcm@openssh.com",
        "aes128-ctr",
        "aes192-ctr",
        "aes256-ctr",
    ];

    /// MAC algorithms (ignored for AEAD ciphers).
    pub const MACS: &[&str] = &[
        "umac-64-etm@openssh.com",
        "umac-128-etm@openssh.com",
        "hmac-sha2-256-etm@openssh.com",
        "hmac-sha2-512-etm@openssh.com",
        "hmac-sha1-etm@openssh.com",
        "umac-64@openssh.com",
        "umac-128@openssh.com",
        "hmac-sha2-256",
        "hmac-sha2-512",
        "hmac-sha1",
    ];

    /// Compression algorithms.
    pub const COMP: &[&str] = &["none"];
}