gunnar-ssh 3.0.0

gunnar's SSH server: a russh listener with publickey identity, a strict git exec dispatch, and the repository path sandbox.
Documentation
//! **The host key a box already has is the one russh serves after the bump**
//! (2026-10-03, finish-bump: russh 0.61 → 0.63, its ssh-key 0.7.0-rc.10 → rc.11).
//!
//! russh parses the stored OpenSSH bytes itself (see `tests/host_key.rs` for why that
//! encoding is the bridge), so this pins what russh's `ssh-key` makes of them: the
//! same bytes for the same key, the same public key, and the same `SHA256:`
//! fingerprint every client has in `known_hosts`. The key is the RFC 8032 section 7.1
//! TEST 1 test vector, minted here — no private key is committed (`xtask
//! key-census`). The expected values were taken on the pre-bump stack and equal
//! `gunnar-core/tests/crypto_kat.rs`'s, so gunnar-core's ssh-key and russh's agree.

use russh::keys::ssh_key::{private::Ed25519Keypair, HashAlg, LineEnding, PrivateKey};

const ED25519_TEST1_SEED_HEX: &str =
    "9d61b19deffd5a60ba844af492ec2cc44449c5697b326919703bac031cae7f60";
const HOST_KEY_ENCODING_SHA256: &str = "SHA256:XMmgeFxuOOPkVX36jCA0uAgw9x7HmpCizqHBw1V/DXY";
const HOST_PUBLIC: &str =
    "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAINdamAGCsQq31Uv+08lkBzoO4XLz2qYjJa8CGmj3B1Ea";
const HOST_FINGERPRINT: &str = "SHA256:bbXpuKG6zhzdmnxq256TlqzFBzRl2f6OOg722cYNbU8";

fn seed() -> [u8; 32] {
    let mut out = [0u8; 32];
    for (i, byte) in out.iter_mut().enumerate() {
        *byte = u8::from_str_radix(&ED25519_TEST1_SEED_HEX[2 * i..2 * i + 2], 16).expect("hex");
    }
    out
}

#[test]
fn russh_reads_and_writes_the_host_key_to_the_same_identity() {
    let built = PrivateKey::new(Ed25519Keypair::from_seed(&seed()).into(), "gunnar-kat")
        .expect("build the key");
    let bytes = built.to_openssh(LineEnding::LF).expect("encode");
    assert_eq!(
        gunnar_core::auth::fingerprint_of_blob(bytes.as_bytes()).as_str(),
        HOST_KEY_ENCODING_SHA256,
        "russh's ssh-key no longer writes the bytes gunnar-core stores"
    );

    let key = PrivateKey::from_openssh(bytes.as_bytes()).expect("russh parses the stored host key");
    assert_eq!(
        key.public_key().fingerprint(HashAlg::Sha256).to_string(),
        HOST_FINGERPRINT
    );
    let line = key.public_key().to_openssh().expect("public line");
    assert_eq!(
        line.split_whitespace()
            .take(2)
            .collect::<Vec<_>>()
            .join(" "),
        HOST_PUBLIC
    );
    // The wire blob the client fingerprints is the one gunnar-core fingerprints.
    let blob = key.public_key().to_bytes().expect("wire blob");
    assert_eq!(
        gunnar_core::auth::fingerprint_of_blob(&blob).as_str(),
        HOST_FINGERPRINT
    );
    let reencoded = key.to_openssh(LineEnding::LF).expect("re-encode");
    assert_eq!(reencoded.as_bytes(), bytes.as_bytes());
}