use alloc::vec::Vec;
use purecrypto::ec::x25519::X25519PrivateKey;
use purecrypto::hash::Sha256;
use purecrypto::rng::{CryptoRng, RngCore};
use super::common::{
KexContext, KexInitOut, KexOutput, SSH_MSG_KEX_ECDH_INIT, SSH_MSG_KEX_ECDH_REPLY,
};
use super::hash::{mpint_bytes, ExchangeHash};
use super::Kex;
use crate::error::{Error, Result};
use crate::format::Reader;
use crate::hostkey::HostKeyVerify;
pub struct Curve25519Sha256;
impl Kex for Curve25519Sha256 {
const NAME: &'static str = "curve25519-sha256";
const HASH_LEN: usize = 32;
}
impl Curve25519Sha256 {
pub const NAME: &'static str = <Self as Kex>::NAME;
pub const NAME_LIBSSH: &'static str = "curve25519-sha256@libssh.org";
pub const HASH_LEN: usize = <Self as Kex>::HASH_LEN;
}
pub struct ClientState {
secret: X25519PrivateKey,
q_c: [u8; 32],
}
pub struct ServerReplyOut {
pub payload: Vec<u8>,
pub kex: KexOutput,
}
impl Curve25519Sha256 {
pub fn client_init<R: RngCore + CryptoRng>(rng: &mut R) -> (ClientState, KexInitOut) {
let secret = X25519PrivateKey::generate(rng);
let q_c = secret.public_key();
let mut payload = Vec::with_capacity(1 + 4 + 32);
payload.push(SSH_MSG_KEX_ECDH_INIT);
payload.extend_from_slice(&(32u32).to_be_bytes());
payload.extend_from_slice(&q_c);
(ClientState { secret, q_c }, KexInitOut { payload })
}
pub fn server_reply<R, S>(
rng: &mut R,
init_payload: &[u8],
host_key: &S,
ctx: &KexContext<'_>,
) -> Result<ServerReplyOut>
where
R: RngCore + CryptoRng,
S: crate::hostkey::HostKey + ?Sized,
{
let mut r = Reader::new(init_payload);
let msg = r.read_u8()?;
if msg != SSH_MSG_KEX_ECDH_INIT {
return Err(Error::Protocol("expected SSH_MSG_KEX_ECDH_INIT"));
}
let q_c_bytes = r.read_string()?;
if q_c_bytes.len() != 32 {
return Err(Error::Format("X25519 Q_C must be 32 bytes"));
}
let mut q_c = [0u8; 32];
q_c.copy_from_slice(q_c_bytes);
let secret = X25519PrivateKey::generate(rng);
let q_s = secret.public_key();
let k_raw = secret
.diffie_hellman(&q_c)
.map_err(|_| Error::Crypto("X25519 small-order peer"))?;
let k_s = host_key.public_blob();
let mut eh = ExchangeHash::<Sha256>::new();
eh.write_string(ctx.v_c);
eh.write_string(ctx.v_s);
eh.write_string(ctx.i_c);
eh.write_string(ctx.i_s);
eh.write_string(&k_s);
eh.write_string(&q_c);
eh.write_string(&q_s);
eh.write_mpint(&k_raw);
let h = eh.finalize();
let sig = host_key.sign(&h)?;
let mut payload = Vec::with_capacity(1 + 4 + k_s.len() + 4 + 32 + 4 + sig.len());
payload.push(SSH_MSG_KEX_ECDH_REPLY);
payload.extend_from_slice(&(k_s.len() as u32).to_be_bytes());
payload.extend_from_slice(&k_s);
payload.extend_from_slice(&(32u32).to_be_bytes());
payload.extend_from_slice(&q_s);
payload.extend_from_slice(&(sig.len() as u32).to_be_bytes());
payload.extend_from_slice(&sig);
let k = mpint_bytes(&k_raw);
Ok(ServerReplyOut {
payload,
kex: KexOutput { k, h },
})
}
pub fn client_finish(
state: ClientState,
reply_payload: &[u8],
verifier: &dyn HostKeyVerify,
ctx: &KexContext<'_>,
) -> Result<KexOutput> {
let mut r = Reader::new(reply_payload);
let msg = r.read_u8()?;
if msg != SSH_MSG_KEX_ECDH_REPLY {
return Err(Error::Protocol("expected SSH_MSG_KEX_ECDH_REPLY"));
}
let k_s = r.read_string()?;
let q_s_bytes = r.read_string()?;
if q_s_bytes.len() != 32 {
return Err(Error::Format("X25519 Q_S must be 32 bytes"));
}
let mut q_s = [0u8; 32];
q_s.copy_from_slice(q_s_bytes);
let sig = r.read_string()?;
let k_raw = state
.secret
.diffie_hellman(&q_s)
.map_err(|_| Error::Crypto("X25519 small-order peer"))?;
let mut eh = ExchangeHash::<Sha256>::new();
eh.write_string(ctx.v_c);
eh.write_string(ctx.v_s);
eh.write_string(ctx.i_c);
eh.write_string(ctx.i_s);
eh.write_string(k_s);
eh.write_string(&state.q_c);
eh.write_string(&q_s);
eh.write_mpint(&k_raw);
let h = eh.finalize();
verifier.verify(&h, sig)?;
let k = mpint_bytes(&k_raw);
Ok(KexOutput { k, h })
}
}
#[cfg(test)]
mod tests {
use super::*;
use purecrypto::hash::Sha256;
use purecrypto::rng::HmacDrbg;
fn hex32(s: &str) -> [u8; 32] {
let mut out = [0u8; 32];
for i in 0..32 {
out[i] = u8::from_str_radix(&s[2 * i..2 * i + 2], 16).unwrap();
}
out
}
#[test]
fn rfc7748_test_vector_1() {
let scalar = hex32("a546e36bf0527c9d3b16154b82465edd62144c0ac1fc5a18506a2244ba449ac4");
let u = hex32("e6db6867583030db3594c1a424b15f7c726624ec26b3353b10a903a6d0ab1c4c");
let want = hex32("c3da55379de9c6908e94ea4df28d084f32eccf03491c71f754b4075577a28552");
let sk = X25519PrivateKey::from_bytes(scalar);
let got = sk.diffie_hellman(&u).unwrap();
assert_eq!(got, want);
}
#[test]
fn algorithm_names() {
assert_eq!(Curve25519Sha256::NAME, "curve25519-sha256");
assert_eq!(
Curve25519Sha256::NAME_LIBSSH,
"curve25519-sha256@libssh.org"
);
assert_eq!(Curve25519Sha256::HASH_LEN, 32);
}
#[test]
fn init_payload_layout() {
let mut rng = HmacDrbg::<Sha256>::new(b"kex-init", b"nonce", &[]);
let (state, init) = Curve25519Sha256::client_init(&mut rng);
assert_eq!(init.payload.len(), 1 + 4 + 32);
assert_eq!(init.payload[0], 30);
assert_eq!(&init.payload[1..5], &[0, 0, 0, 32]);
assert_eq!(&init.payload[5..], &state.q_c[..]);
}
}