12 KiB
Russh: Cryptographic Primitives
This document covers the cryptographic implementations in russh — key exchange algorithms, ciphers, MACs, and key handling.
Key Exchange Algorithms (kex module)
All KEX algorithms implement the KexAlgorithmImplementor trait:
pub(crate) trait KexAlgorithmImplementor {
fn skip_exchange(&self) -> bool;
fn is_dh_gex(&self) -> bool;
fn client_dh_gex_init(&mut self, gex: &GexParams, writer: &mut impl Writer) -> Result<(), Error>;
fn dh_gex_set_group(&mut self, group: DhGroup) -> Result<(), Error>;
fn server_dh(&mut self, exchange: &mut Exchange, payload: &[u8]) -> Result<(), Error>;
fn client_dh(&mut self, client_ephemeral: &mut Vec<u8>, writer: &mut impl Writer) -> Result<(), Error>;
fn compute_shared_secret(&mut self, remote_pubkey: &[u8]) -> Result<(), Error>;
fn shared_secret_bytes(&self) -> Option<&[u8]>;
fn compute_exchange_hash(&self, key: &[u8], exchange: &Exchange, buffer: &mut CryptoVec) -> Result<Vec<u8>, Error>;
fn compute_keys(&self, session_id: &[u8], exchange_hash: &[u8], cipher: cipher::Name, remote_to_local_mac: mac::Name, local_to_remote_mac: mac::Name, is_server: bool) -> Result<CipherPair, Error>;
}
The KexAlgorithm enum dispatches via enum_dispatch:
pub(crate) enum KexAlgorithm {
DhGroupKexSha1(DhGroupKex<Sha1>),
DhGroupKexSha256(DhGroupKex<Sha256>),
DhGroupKexSha512(DhGroupKex<Sha512>),
Curve25519Kex(Curve25519Kex),
EcdhNistP256Kex(EcdhNistPKex<NistP256, Sha256>),
EcdhNistP384Kex(EcdhNistPKex<NistP384, Sha384>),
EcdhNistP521Kex(EcdhNistPKex<NistP521, Sha512>),
MlKem768X25519Kex(MlKem768X25519Kex),
NoneKexAlgorithm(NoneKexAlgorithm),
}
Supported KEX Algorithms
| Algorithm Name | Constant | Type | Hash | Notes |
|---|---|---|---|---|
mlkem768x25519-sha256 |
MLKEM768X25519_SHA256 |
PQ/T Hybrid (ML-KEM + X25519) | SHA-256 | Default first choice, post-quantum |
curve25519-sha256 |
CURVE25519 |
Curve25519 ECDH | SHA-256 | Recommended |
curve25519-sha256@libssh.org |
CURVE25519_PRE_RFC_8731 |
Curve25519 ECDH | SHA-256 | Pre-RFC name, same impl |
ecdh-sha2-nistp256 |
ECDH_SHA2_NISTP256 |
NIST P-256 ECDH | SHA-256 | |
ecdh-sha2-nistp384 |
ECDH_SHA2_NISTP384 |
NIST P-384 ECDH | SHA-384 | |
ecdh-sha2-nistp521 |
ECDH_SHA2_NISTP521 |
NIST P-521 ECDH | SHA-512 | |
diffie-hellman-group-exchange-sha256 |
DH_GEX_SHA256 |
DH-GEX | SHA-256 | Dynamic group |
diffie-hellman-group-exchange-sha1 |
DH_GEX_SHA1 |
DH-GEX | SHA-1 | Legacy |
diffie-hellman-group14-sha256 |
DH_G14_SHA256 |
DH Group 14 | SHA-256 | 2048-bit |
diffie-hellman-group14-sha1 |
DH_G14_SHA1 |
DH Group 14 | SHA-1 | Legacy |
diffie-hellman-group15-sha512 |
DH_G15_SHA512 |
DH Group 15 | SHA-512 | 3072-bit |
diffie-hellman-group16-sha512 |
DH_G16_SHA512 |
DH Group 16 | SHA-512 | 4096-bit |
diffie-hellman-group17-sha512 |
DH_G17_SHA512 |
DH Group 17 | SHA-512 | 6144-bit |
diffie-hellman-group18-sha512 |
DH_G18_SHA512 |
DH Group 18 | SHA-512 | 8192-bit |
diffie-hellman-group1-sha1 |
DH_G1_SHA1 |
DH Group 1 | SHA-1 | Insecure, 1024-bit |
none |
NONE |
No exchange | N/A | Testing only |
Curve25519 Implementation
Located in kex/curve25519.rs. Uses curve25519-dalek for the Diffie-Hellman computation:
- Generates an ephemeral keypair
- Sends the public key as the DH init
- Computes the shared secret from the remote public key
- The shared secret is encoded as a raw 32-byte string (not mpint) in the exchange hash
NIST ECDH Implementation
Located in kex/ecdh_nistp.rs. Generic over the curve (NistP256, NistP384, NistP521):
- Uses
p256/p384/p521crates for ECDH - Points are encoded in the SSH EC point format (32/48/66 bytes for uncompressed)
DH Group Exchange Implementation
Located in kex/dh/. Two variants: DhGroupKex<D> (GEX) and fixed-group variants.
- GEX uses
num-bigintfor modular exponentiation with safe primes - Fixed groups use pre-defined safe primes from RFC 3526
DhGroupstruct:{ prime: CryptoVec, generator: CryptoVec }- Server provides groups via
Handler::lookup_dh_gex_group()— default usesBUILTIN_SAFE_DH_GROUPS GexParamscontrols min/preferred/max group sizes (default: 3072/8192/8192 bits)
ML-KEM Hybrid Implementation
Located in kex/hybrid_mlkem.rs. Implements the mlkem768x25519-sha256 hybrid key exchange:
- Combines ML-KEM-768 (post-quantum) with X25519 (classical)
- Both shared secrets are concatenated before hashing
- Provides quantum-resistant key exchange while maintaining classical security
Exchange Hash Computation
The exchange hash H is computed from the following data (per RFC 4253 §8):
H = HASH(V_C || V_S || I_C || I_S || K_S || e || f || K)
Where:
V_C= client version stringV_S= server version stringI_C= client's KEXINIT payloadI_S= server's KEXINIT payloadK_S= server host keye= client ephemeral public keyf= server ephemeral public keyK= shared secret
For DH-GEX, additional fields are included (p, g, and the GEX parameters).
Ciphers (cipher module)
The Cipher trait defines how to construct opening and sealing keys:
pub(crate) trait Cipher {
fn needs_mac(&self) -> bool; // Whether a separate MAC is needed
fn key_len(&self) -> usize;
fn nonce_len(&self) -> usize;
fn make_opening_key(&self, key, nonce, mac_key, mac) -> Box<dyn OpeningKey + Send>;
fn make_sealing_key(&self, key, nonce, mac_key, mac) -> Box<dyn SealingKey + Send>;
}
Supported Ciphers
| Algorithm Name | Constant | Type | Key Size | MAC Required | AEAD |
|---|---|---|---|---|---|
chacha20-poly1305@openssh.com |
CHACHA20_POLY1305 |
Stream cipher + Poly1305 | 256-bit + 256-bit | No | Yes |
aes256-gcm@openssh.com |
AES_256_GCM |
AES-GCM | 256-bit | No | Yes |
aes128-gcm@openssh.com |
AES_128_GCM |
AES-GCM | 128-bit | No | Yes |
aes256-ctr |
AES_256_CTR |
AES-CTR | 256-bit | Yes | No |
aes192-ctr |
AES_192_CTR |
AES-CTR | 192-bit | Yes | No |
aes128-ctr |
AES_128_CTR |
AES-CTR | 128-bit | Yes | No |
aes256-cbc |
AES_256_CBC |
AES-CBC | 256-bit | Yes | No |
aes192-cbc |
AES_192_CBC |
AES-CBC | 192-bit | Yes | No |
aes128-cbc |
AES_128_CBC |
AES-CBC | 128-bit | Yes | No |
3des-cbc |
TRIPLE_DES_CBC |
3DES-CBC | 168-bit | Yes | No |
AEAD Ciphers (Chacha20-Poly1305, AES-GCM)
AEAD ciphers do not need a separate MAC. They handle both encryption and authentication:
- Chacha20-Poly1305: Uses the OpenSSH construction where the sequence number is used as the Chacha20 counter. The Poly1305 key is derived from a separate Chacha20 stream.
- AES-GCM: Uses
aws-lc-rsorringas the backend depending on feature flags. The nonce is constructed from the sequence number.
Block Ciphers (AES-CTR, AES-CBC, 3DES-CBC)
Block ciphers require a separate MAC. They implement SshBlockCipher<C>:
- CTR mode: Uses
ctr::Ctr128BEfrom thectrcrate - CBC mode: Uses
cbc::Encryptor/cbc::Decryptorfrom thecbccrate with PKCS#7 padding - The
needs_mac()method returnstrue
OpeningKey and SealingKey Traits
pub(crate) trait OpeningKey {
fn packet_length_to_read_for_block_length(&self) -> usize { 4 }
fn decrypt_packet_length(&self, seqn: u32, encrypted_packet_length: &[u8]) -> [u8; 4];
fn tag_len(&self) -> usize;
fn open<'a>(&mut self, seqn: u32, ciphertext_and_tag: &'a mut [u8]) -> Result<&'a [u8], Error>;
}
pub(crate) trait SealingKey {
fn padding_length(&self, plaintext: &[u8]) -> usize;
fn fill_padding(&self, padding_out: &mut [u8]);
fn tag_len(&self) -> usize;
fn seal(&mut self, seqn: u32, plaintext_in_ciphertext_out: &mut [u8], tag_out: &mut [u8]);
fn write(&mut self, payload: &[u8], buffer: &mut SSHBuffer);
}
The write() method on SealingKey handles the full packet construction:
- Compute padding length (minimum 4 bytes, block-aligned)
- Write packet length (4 bytes) + padding length (1 byte) + payload + padding
- Encrypt the packet
- Append the authentication tag
- Increment the sequence number
MACs (mac module)
MAC algorithms are split into two categories: regular and Encrypt-Then-MAC (ETM).
Supported MACs
| Algorithm Name | Constant | Hash | Key Length | ETM |
|---|---|---|---|---|
hmac-sha2-512-etm@openssh.com |
HMAC_SHA512_ETM |
SHA-512 | 64 bytes | Yes |
hmac-sha2-256-etm@openssh.com |
HMAC_SHA256_ETM |
SHA-256 | 32 bytes | Yes |
hmac-sha2-512 |
HMAC_SHA512 |
SHA-512 | 64 bytes | No |
hmac-sha2-256 |
HMAC_SHA256 |
SHA-256 | 32 bytes | No |
hmac-sha1-etm@openssh.com |
HMAC_SHA1_ETM |
SHA-1 | 20 bytes | Yes |
hmac-sha1 |
HMAC_SHA1 |
SHA-1 | 20 bytes | No |
none |
NONE |
— | 0 | — |
ETM (Encrypt-Then-MAC) Mode
With ETM MACs:
- The packet length field is sent unencrypted (read first)
- The rest of the packet is encrypted
- The MAC is computed over the unencrypted length + encrypted payload
- This prevents padding oracle attacks
Regular MAC Mode
With regular MACs:
- The entire packet (including length) is encrypted
- The MAC is computed over the sequence number + unencrypted packet
- This is the traditional SSH MAC construction
Key Handling (keys module)
Key Formats Supported
- OpenSSH format (private keys):
-----BEGIN OPENSSH PRIVATE KEY-----with bcrypt-pbkdf encrypted keys - PKCS#8 (unencrypted and encrypted):
-----BEGIN PRIVATE KEY-----/-----BEGIN ENCRYPTED PRIVATE KEY----- - PKCS#1 (RSA):
-----BEGIN RSA PRIVATE KEY----- - SEC1 (EC):
-----BEGIN EC PRIVATE KEY----- - OpenSSH public keys:
ssh-ed25519 AAAAC3N... - PPK format: PuTTY private key files
- OpenSSH certificates:
-----BEGIN OPENSSH SSH2 CERTIFICATE-----
Key Algorithms
| Algorithm | Key Type | Signing |
|---|---|---|
ssh-ed25519 |
Ed25519 | Ed25519 |
ecdsa-sha2-nistp256 |
ECDSA P-256 | SHA-256 |
ecdsa-sha2-nistp384 |
ECDSA P-384 | SHA-384 |
ecdsa-sha2-nistp521 |
ECDSA P-521 | SHA-512 |
rsa-sha2-512 |
RSA | SHA-512 |
rsa-sha2-256 |
RSA | SHA-256 |
ssh-rsa |
RSA | SHA-1 (legacy) |
PrivateKeyWithHashAlg
Wrapper that pairs a private key with a specific hash algorithm (needed for RSA key disambiguation):
pub struct PrivateKeyWithHashAlg {
key: Arc<PrivateKey>,
hash: Option<HashAlg>,
}
This is required because RSA keys can sign with different hash algorithms (rsa-sha2-256, rsa-sha2-512, ssh-rsa), and the server needs to know which one the client intends.
SSH Agent Protocol (keys::agent)
Russh implements both the client and server sides of the SSH agent protocol:
Client (agent::client::AgentClient):
impl<R: AsyncRead + AsyncWrite + Unpin + Send + 'static> AgentClient<R> {
pub async fn request_identities(&mut self) -> Result<Vec<AgentIdentity>, Error>;
pub async fn sign_request(&mut self, key: &AgentIdentity, hash_alg: Option<HashAlg>, data: Vec<u8>) -> Result<Vec<u8>, Error>;
pub async fn add_identity(&mut self, key: &PrivateKey, constraints: &[Constraint]) -> Result<(), Error>;
pub async fn remove_identity(&mut self, key: &PublicKey) -> Result<(), Error>;
pub async fn remove_all_identities(&mut self) -> Result<(), Error>;
pub async fn lock(&mut self, passphrase: &str) -> Result<(), Error>;
pub async fn unlock(&mut self, passphrase: &str) -> Result<(), Error>;
// ... ping, add smartcard, etc.
}
Server (agent::server::Agent):
pub trait Agent: Clone + Send + 'static {
fn confirm(self, key: Arc<PrivateKey>) -> Box<dyn Future<Output = (Self, bool)> + Send + Unpin>;
}
AgentIdentity distinguishes between plain public keys and certificates:
pub enum AgentIdentity {
PublicKey { pubkey: PublicKey, comment: String },
Certificate { certificate: Certificate, comment: String },
}
Known Hosts (keys::known_hosts)
pub fn check_known_hosts(host: &str, port: u16, key: &PublicKey) -> Result<(), Error>;
pub fn check_known_hosts_path<P: AsRef<Path>>(path: P, host: &str, port: u16, key: &PublicKey) -> Result<(), Error>;
These functions read ~/.ssh/known_hosts and verify the server's public key matches. Returns Error::KeyChanged on mismatch.
Windows Pageant Support
On Windows, russh uses the pageant crate to communicate with the Pageant SSH agent via either:
- WM_MESSAGE-based protocol (legacy)
- Named pipes protocol (since PuTTY 0.75)