metalssh 0.0.1

Experimental SSH implementation
//! `curve25519-sha256` key exchange algorithm.
//!
//! See [RFC 8731][1].
//!
//! [1]: https://datatracker.ietf.org/doc/html/rfc8731

use aws_lc_rs::agreement::EphemeralPrivateKey;
use aws_lc_rs::agreement::PublicKey;
use aws_lc_rs::agreement::UnparsedPublicKey;
use aws_lc_rs::agreement::X25519;
use aws_lc_rs::agreement::agree_ephemeral;
use aws_lc_rs::rand::SystemRandom;

use crate::crypto::kex::Initiate;
use crate::crypto::kex::Reply;
use crate::types::Error;
use crate::types::Result;
use crate::types::Vec;

pub struct Initiator {
    private_key: EphemeralPrivateKey,
    public_key: PublicKey,
}

pub struct Replier {}

impl Initiate for Initiator {
    fn new() -> Result<Self> {
        let rng = SystemRandom::new();

        let private_key = EphemeralPrivateKey::generate(&X25519, &rng)?;
        let public_key = private_key.compute_public_key()?;

        Ok(Self {
            private_key,
            public_key,
        })
    }

    fn public_key(&self) -> &[u8] {
        self.public_key.as_ref()
    }

    fn agree(self, peer_public_key: &[u8]) -> Result<Vec<u8, 128>> {
        let peer_public_key = UnparsedPublicKey::new(&X25519, peer_public_key);

        let shared_secret = agree_ephemeral(
            self.private_key,
            peer_public_key,
            Error::Crypto,
            |key_material| Ok(make_shared_secret(key_material)),
        )?;

        Ok(shared_secret)
    }
}

impl Reply for Replier {
    fn agree(peer_public_key: &[u8]) -> Result<(Vec<u8, 128>, Vec<u8, 2048>)> {
        let rng = SystemRandom::new();

        let private_key = EphemeralPrivateKey::generate(&X25519, &rng)?;
        let public_key = private_key.compute_public_key()?;

        let mut public_key_bytes = Vec::new();
        public_key_bytes.extend_from_slice(public_key.as_ref());

        let peer_public_key = UnparsedPublicKey::new(&X25519, peer_public_key);

        let shared_secret = agree_ephemeral(
            private_key,
            peer_public_key,
            Error::Crypto,
            |key_material| Ok(make_shared_secret(key_material)),
        )?;

        Ok((shared_secret, public_key_bytes))
    }
}

// Shared secret requires special handling as an mpint to ensure it is not
// interpreted as a negative number
// https://datatracker.ietf.org/doc/html/rfc8731#name-shared-secret-encoding

// Check if high bit is set, which would mean the shared secret needs padding.
// If this was not done, SSH libraries would decode this as a negative number,
// which is not how curve25519-sha256 expects
fn make_shared_secret(key_material: &[u8]) -> Vec<u8, 128> {
    let needs_padding = key_material[0] & 0x80 != 0;

    let mut shared_secret = Vec::new();

    // TODO: May be preferable to use proto::SshWritable
    if needs_padding {
        let len = (key_material.len() + 1) as u32;
        shared_secret.extend_from_slice(&len.to_be_bytes());
        shared_secret.push(0x00);
        shared_secret.extend_from_slice(key_material);
    } else {
        let len = key_material.len() as u32;
        shared_secret.extend_from_slice(&len.to_be_bytes());
        shared_secret.extend_from_slice(key_material);
    }

    shared_secret
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn kex_works() {
        let client = Initiator::new().unwrap();

        let client_public_key = client.public_key().to_owned();

        let (server_shared_secret, server_public_key) = Replier::agree(&client_public_key).unwrap();

        let client_shared_secret = client.agree(&server_public_key).unwrap();

        assert_eq!(client_shared_secret.len(), server_shared_secret.len());
        assert_eq!(client_shared_secret.as_ref(), server_shared_secret.as_ref());
    }
}