diff --git a/assets/kex/kex-hybrid/ecdh-nistp256-kyber-512r3-sha256-d00/client_kex_init.raw b/assets/kex/kex-hybrid/ecdh-nistp256-kyber-512r3-sha256-d00/client_kex_init.raw new file mode 100644 index 0000000..8af5c6c Binary files /dev/null and b/assets/kex/kex-hybrid/ecdh-nistp256-kyber-512r3-sha256-d00/client_kex_init.raw differ diff --git a/assets/kex/kex-hybrid/ecdh-nistp256-kyber-512r3-sha256-d00/init.raw b/assets/kex/kex-hybrid/ecdh-nistp256-kyber-512r3-sha256-d00/init.raw new file mode 100644 index 0000000..99a5398 Binary files /dev/null and b/assets/kex/kex-hybrid/ecdh-nistp256-kyber-512r3-sha256-d00/init.raw differ diff --git a/assets/kex/kex-hybrid/ecdh-nistp256-kyber-512r3-sha256-d00/reply.raw b/assets/kex/kex-hybrid/ecdh-nistp256-kyber-512r3-sha256-d00/reply.raw new file mode 100644 index 0000000..1e24d6e Binary files /dev/null and b/assets/kex/kex-hybrid/ecdh-nistp256-kyber-512r3-sha256-d00/reply.raw differ diff --git a/assets/kex/kex-hybrid/ecdh-nistp256-kyber-512r3-sha256-d00/server_kex_init.raw b/assets/kex/kex-hybrid/ecdh-nistp256-kyber-512r3-sha256-d00/server_kex_init.raw new file mode 100644 index 0000000..1a66080 Binary files /dev/null and b/assets/kex/kex-hybrid/ecdh-nistp256-kyber-512r3-sha256-d00/server_kex_init.raw differ diff --git a/assets/kex/kex-hybrid/ecdh-nistp384-kyber-768r3-sha384-d00/client_kex_init.raw b/assets/kex/kex-hybrid/ecdh-nistp384-kyber-768r3-sha384-d00/client_kex_init.raw new file mode 100644 index 0000000..0120d73 Binary files /dev/null and b/assets/kex/kex-hybrid/ecdh-nistp384-kyber-768r3-sha384-d00/client_kex_init.raw differ diff --git a/assets/kex/kex-hybrid/ecdh-nistp384-kyber-768r3-sha384-d00/init.raw b/assets/kex/kex-hybrid/ecdh-nistp384-kyber-768r3-sha384-d00/init.raw new file mode 100644 index 0000000..cf29220 Binary files /dev/null and b/assets/kex/kex-hybrid/ecdh-nistp384-kyber-768r3-sha384-d00/init.raw differ diff --git a/assets/kex/kex-hybrid/ecdh-nistp384-kyber-768r3-sha384-d00/reply.raw b/assets/kex/kex-hybrid/ecdh-nistp384-kyber-768r3-sha384-d00/reply.raw new file mode 100644 index 0000000..11be790 Binary files /dev/null and b/assets/kex/kex-hybrid/ecdh-nistp384-kyber-768r3-sha384-d00/reply.raw differ diff --git a/assets/kex/kex-hybrid/ecdh-nistp384-kyber-768r3-sha384-d00/server_kex_init.raw b/assets/kex/kex-hybrid/ecdh-nistp384-kyber-768r3-sha384-d00/server_kex_init.raw new file mode 100644 index 0000000..6db4a57 Binary files /dev/null and b/assets/kex/kex-hybrid/ecdh-nistp384-kyber-768r3-sha384-d00/server_kex_init.raw differ diff --git a/assets/kex/kex-hybrid/ecdh-nistp521-kyber-1024r3-sha512-d00/client_kex_init.raw b/assets/kex/kex-hybrid/ecdh-nistp521-kyber-1024r3-sha512-d00/client_kex_init.raw new file mode 100644 index 0000000..6d5598b Binary files /dev/null and b/assets/kex/kex-hybrid/ecdh-nistp521-kyber-1024r3-sha512-d00/client_kex_init.raw differ diff --git a/assets/kex/kex-hybrid/ecdh-nistp521-kyber-1024r3-sha512-d00/init.raw b/assets/kex/kex-hybrid/ecdh-nistp521-kyber-1024r3-sha512-d00/init.raw new file mode 100644 index 0000000..a1f319c Binary files /dev/null and b/assets/kex/kex-hybrid/ecdh-nistp521-kyber-1024r3-sha512-d00/init.raw differ diff --git a/assets/kex/kex-hybrid/ecdh-nistp521-kyber-1024r3-sha512-d00/reply.raw b/assets/kex/kex-hybrid/ecdh-nistp521-kyber-1024r3-sha512-d00/reply.raw new file mode 100644 index 0000000..2ab9574 Binary files /dev/null and b/assets/kex/kex-hybrid/ecdh-nistp521-kyber-1024r3-sha512-d00/reply.raw differ diff --git a/assets/kex/kex-hybrid/ecdh-nistp521-kyber-1024r3-sha512-d00/server_kex_init.raw b/assets/kex/kex-hybrid/ecdh-nistp521-kyber-1024r3-sha512-d00/server_kex_init.raw new file mode 100644 index 0000000..ec041d0 Binary files /dev/null and b/assets/kex/kex-hybrid/ecdh-nistp521-kyber-1024r3-sha512-d00/server_kex_init.raw differ diff --git a/src/kex.rs b/src/kex.rs index c6375ea..5a5bacb 100644 --- a/src/kex.rs +++ b/src/kex.rs @@ -14,7 +14,8 @@ #[cfg(feature = "integers")] use std::marker::PhantomData; -use nom::combinator::{all_consuming, map}; +use nom::bytes::complete::take; +use nom::combinator::{all_consuming, map, map_parser, rest}; use nom::error::Error; use nom::number::streaming::be_u32; use nom::sequence::{pair, tuple}; @@ -62,9 +63,50 @@ pub const SSH_MSG_KEX_DH_GEX_INIT: u8 = 32; /// Defined in [RFC4419 section 5](https://datatracker.ietf.org/doc/html/rfc4419#section-5). pub const SSH_MSG_KEX_DH_GEX_REPLY: u8 = 33; +/// PQ/T Hybrid Key Exchange Init message code. +/// Defined in [draft RFC `draft-kampanakis-curdle-ssh-pq-ke-02` section 2.2](https://www.ietf.org/archive/id/draft-kampanakis-curdle-ssh-pq-ke-02.html#section-2.2) +pub const SSH_MSG_KEX_HYBRID_INIT: u8 = 30; + +/// PQ/T Hybrid Key Exchange Reply message code. +/// Defined in [draft RFC `draft-kampanakis-curdle-ssh-pq-ke-02` section 2.2](https://www.ietf.org/archive/id/draft-kampanakis-curdle-ssh-pq-ke-02.html#section-2.2) +pub const SSH_MSG_KEX_HYBRID_REPLY: u8 = 31; + +/// Supported PQ/T Hybrid Key Exchange algorithm. +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub enum SupportedHybridKEXAlgorithm { + /// ecdh-nistp256-kyber-512r3-sha256-d00@openquantumsafe.org + ECDHNistP256Kyber512r3Sha256D00OQS, + + /// ecdh-nistp384-kyber-768r3-sha384-d00@openquantumsafe.org + ECDHNistP384Kyber768r3Sha384D00OQS, + + /// ecdh-nistp521-kyber-1024r3-sha512-d00@openquantumsafe.org + ECDHNistP521Kyber1024r3Sha512D00OQS, +} + +impl SupportedHybridKEXAlgorithm { + /// Returns the length in bytes of the post-quantum KEM public key. + pub fn pq_pub_key_len(self) -> usize { + match self { + Self::ECDHNistP256Kyber512r3Sha256D00OQS => 800, + Self::ECDHNistP384Kyber768r3Sha384D00OQS => 1184, + Self::ECDHNistP521Kyber1024r3Sha512D00OQS => 1568, + } + } + + /// Returns the length in bytes of the ciphertext produced by the KEM algorithm. + pub fn pq_ciphertext_len(self) -> usize { + match self { + Self::ECDHNistP256Kyber512r3Sha256D00OQS => 768, + Self::ECDHNistP384Kyber768r3Sha384D00OQS => 1088, + Self::ECDHNistP521Kyber1024r3Sha512D00OQS => 1568, + } + } +} + #[cfg(feature = "integers")] fn parse_mpint(i: &[u8]) -> IResult<&[u8], BigInt> { - nom::combinator::map_parser(parse_string, crate::mpint::parse_ssh_mpint)(i) + map_parser(parse_string, crate::mpint::parse_ssh_mpint)(i) } #[cfg(not(feature = "integers"))] @@ -507,6 +549,100 @@ pub struct SshKEXDiffieHellmanKEXGEX<'a> { pub reply: Option>, } +/// SSH Hybrid Key Exchange init. +/// +/// The message code is `SSH_MSG_KEX_HYBRID_INIT`, defined in +/// [draft RFC `draft-kampanakis-curdle-ssh-pq-ke-02` section 2.2](https://www.ietf.org/archive/id/draft-kampanakis-curdle-ssh-pq-ke-02.html#section-2.2) +#[derive(Debug, PartialEq)] +pub struct SshPacketHybridKEXInit<'a> { + /// The post-quantum KEM's public key (`C_PK2`). + pub pq_pub_key: &'a [u8], + + /// The traditional / classical KEX public key. + pub classical_pub_key: &'a [u8], +} + +impl<'a> SshPacketHybridKEXInit<'a> { + /// Parses a SSH PQ/T Hybrid Key Exchange Init. + pub fn parse(i: &'a [u8], alg: SupportedHybridKEXAlgorithm) -> IResult<&'a [u8], Self> { + let pq_len = alg.pq_pub_key_len(); + let (i, (pq_pub_key, classical_pub_key)) = + map_parser(parse_string, tuple((take(pq_len), rest)))(i)?; + Ok(( + i, + Self { + pq_pub_key, + classical_pub_key, + }, + )) + } +} + +/// SSH Hybrid Key Exchange reply. +/// +/// The message code is `SSH_MSG_KEX_HYBRID_REPLY`, defined in +/// [draft RFC `draft-kampanakis-curdle-ssh-pq-ke-02` section 2.2](https://www.ietf.org/archive/id/draft-kampanakis-curdle-ssh-pq-ke-02.html#section-2.2) +#[derive(Debug, PartialEq)] +pub struct SshPacketHybridKEXReply<'a> { + /// K_S, server's public host key. + pub pubkey_and_cert: &'a [u8], + + /// S_CT2, the ciphertext 'ct' output of the corresponding KEM's 'Encaps' algorithm. + pub pq_ciphertext: &'a [u8], + + /// S_PK1, ephemeral (EC)DH server public key. + pub classical_pub_key: &'a [u8], + + /// Signature. + pub signature: &'a [u8], +} + +impl<'a> SshPacketHybridKEXReply<'a> { + /// Parses a SSH PQ/T Hybrid Key Exchange reply. + pub fn parse(i: &'a [u8], alg: SupportedHybridKEXAlgorithm) -> IResult<&'a [u8], Self> { + let ct_len = alg.pq_ciphertext_len(); + let (i, (pubkey_and_cert, (pq_ciphertext, classical_pub_key), signature)) = tuple(( + parse_string, + map_parser(parse_string, tuple((take(ct_len), rest))), + parse_string, + ))(i)?; + Ok(( + i, + Self { + pubkey_and_cert, + pq_ciphertext, + classical_pub_key, + signature, + }, + )) + } +} + +/// The key exchange protocol using PQ/T Key Exchange, defined in +/// [draft RFC `draft-kampanakis-curdle-ssh-pq-ke-02`](https://www.ietf.org/archive/id/draft-kampanakis-curdle-ssh-pq-ke-02.html). +#[derive(Debug, PartialEq)] +pub struct SshHybridKEX<'a> { + /// The init message, i.e. `SSH_MSG_KEX_HYBRID_INIT`. + pub init: Option>, + + /// The reply message, i.e. `SSH_MSG_KEX_HYBRID_REPLY`. + pub reply: Option>, + + /// The algorithm. + pub alg: SupportedHybridKEXAlgorithm, +} + +impl SshHybridKEX<'_> { + /// Initializes a new [`SshHybridKEX`] using the given algorithm. + pub fn new(alg: SupportedHybridKEXAlgorithm) -> Self { + Self { + init: None, + reply: None, + alg, + } + } +} + /// An error occurring in the KEX parser. #[derive(Debug)] pub enum SshKEXError<'a> { @@ -575,6 +711,23 @@ macro_rules! parse_match_and_assign { }; } +/// Parses a hybrid KEX message, matches its owner and assign the parsed +/// object to it. +/// +/// We use a macro here because we take a field of `SshHybridKEX` as a parameter +/// (the receiver). +macro_rules! parse_match_and_assign_hybrid { + ($variant:ident, $field:ident, $struct:ident, $payload:ident) => { + if $variant.$field.is_some() { + Err(SshKEXError::DuplicatedMessage) + } else { + let alg = $variant.alg; + $variant.$field = Some(all_consuming(|i| $struct::parse(i, alg))($payload)?.1); + Ok(()) + } + }; +} + /// Negociates the KEX algorithm. pub fn ssh_kex_negociate_algorithm<'a, 'b, 'c, S1, S2>( client_kex_algs: impl IntoIterator, @@ -607,6 +760,10 @@ pub enum SshKEX<'a> { /// Diffie Hellman Group and Key, defined in RFC4419. DiffieHellmanKEXGEX(SshKEXDiffieHellmanKEXGEX<'a>), + + /// PQ/T Hybrid Key Exchange, defined in + /// [draft RFC `draft-kampanakis-curdle-ssh-pq-ke-02`](https://www.ietf.org/archive/id/draft-kampanakis-curdle-ssh-pq-ke-02.html). + HybridKEX(SshHybridKEX<'a>), } impl<'a> SshKEX<'a> { @@ -642,6 +799,15 @@ impl<'a> SshKEX<'a> { "diffie-hellman-group-exchange-sha1" | "diffie-hellman-group-exchange-sha256" => Ok( Self::DiffieHellmanKEXGEX(SshKEXDiffieHellmanKEXGEX::default()), ), + "ecdh-nistp256-kyber-512r3-sha256-d00@openquantumsafe.org" => Ok(Self::HybridKEX( + SshHybridKEX::new(SupportedHybridKEXAlgorithm::ECDHNistP256Kyber512r3Sha256D00OQS), + )), + "ecdh-nistp384-kyber-768r3-sha384-d00@openquantumsafe.org" => Ok(Self::HybridKEX( + SshHybridKEX::new(SupportedHybridKEXAlgorithm::ECDHNistP384Kyber768r3Sha384D00OQS), + )), + "ecdh-nistp521-kyber-1024r3-sha512-d00@openquantumsafe.org" => Ok(Self::HybridKEX( + SshHybridKEX::new(SupportedHybridKEXAlgorithm::ECDHNistP521Kyber1024r3Sha512D00OQS), + )), _ => Err(SshKEXError::UnknownProtocol), } .map(|kex| (kex, negociated_alg)) @@ -695,6 +861,15 @@ impl<'a> SshKEX<'a> { } _ => Err(SshKEXError::UnexpectedMessage), }, + Self::HybridKEX(hk) => match unparsed_ssh_packet.message_code { + SSH_MSG_KEX_HYBRID_INIT => { + parse_match_and_assign_hybrid!(hk, init, SshPacketHybridKEXInit, payload) + } + SSH_MSG_KEX_HYBRID_REPLY => { + parse_match_and_assign_hybrid!(hk, reply, SshPacketHybridKEXReply, payload) + } + _ => Err(SshKEXError::UnexpectedMessage), + }, } } } diff --git a/src/lib.rs b/src/lib.rs index 52d5dbf..3148136 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -16,6 +16,7 @@ pub use kex::{ SshKEXDiffieHellmanKEXGEX, SshKEXECDiffieHellman, SshKEXError, SshPacketDHKEXInit, SshPacketDHKEXReply, SshPacketDhKEXGEXGroup, SshPacketDhKEXGEXInit, SshPacketDhKEXGEXReply, SshPacketDhKEXGEXRequest, SshPacketDhKEXGEXRequestOld, SshPacketECDHKEXInit, - SshPacketECDHKEXReply, + SshPacketECDHKEXReply, SshPacketHybridKEXInit, SshPacketHybridKEXReply, + SupportedHybridKEXAlgorithm, }; pub use ssh::*; diff --git a/tests/tests_kex.rs b/tests/tests_kex.rs index 43f77e3..784ee7e 100644 --- a/tests/tests_kex.rs +++ b/tests/tests_kex.rs @@ -179,6 +179,85 @@ mod dh_kex_gex { } } +mod kex_hybrid_oqs { + use std::fs; + use std::path::Path; + + use super::*; + + /// Path to assets. + const ASSETS_PATH: &str = "assets/kex/kex-hybrid"; + + fn read_test_file(directory: &Path, filename: &str) -> &'static [u8] { + let data = Box::new(fs::read(directory.join(filename)).unwrap()); + Box::leak(data) + } + + /// Tests an hybrid algorithm with a directory containing its assets. + fn test_alg_with_directory(directory: impl AsRef, expected_algorithm: impl AsRef) { + let directory = Path::new(ASSETS_PATH).join(directory); + println!("dir={}", directory.display()); + let client_kex_init = read_test_file(&directory, "client_kex_init.raw"); + let server_kex_init = read_test_file(&directory, "server_kex_init.raw"); + + let init_msg = fs::read(directory.join("init.raw")).unwrap(); + let reply_msg = fs::read(directory.join("reply.raw")).unwrap(); + + let (client_kex, server_kex) = + load_client_server_key_exchange_init(client_kex_init, server_kex_init); + + let (mut kex, negotiated_alg) = SshKEX::init(&client_kex, &server_kex).unwrap(); + assert_eq!(negotiated_alg, expected_algorithm.as_ref()); + assert!(matches!(kex, SshKEX::HybridKEX(_))); + + let init_packet = load_kex_packet(&init_msg); + assert!(matches!(kex.parse_ssh_packet(&init_packet), Ok(()))); + assert!(matches!( + kex.parse_ssh_packet(&init_packet), + Err(SshKEXError::DuplicatedMessage) + )); + + let reply_packet = load_kex_packet(&reply_msg); + assert!(matches!(kex.parse_ssh_packet(&reply_packet), Ok(()))); + assert!(matches!( + kex.parse_ssh_packet(&reply_packet), + Err(SshKEXError::DuplicatedMessage) + )); + + let kex = match kex { + SshKEX::HybridKEX(kex) => kex, + _ => unreachable!(), + }; + + assert!(kex.init.is_some()); + assert!(kex.reply.is_some()); + } + + #[test] + fn ecdh_nistp256_kyber_512r3_sha256_d00_openquantumsafe_org_test() { + test_alg_with_directory( + "ecdh-nistp256-kyber-512r3-sha256-d00", + "ecdh-nistp256-kyber-512r3-sha256-d00@openquantumsafe.org", + ); + } + + #[test] + fn ecdh_nistp384_kyber_768r3_sha384_d00_openquantumsafe_org_test() { + test_alg_with_directory( + "ecdh-nistp384-kyber-768r3-sha384-d00", + "ecdh-nistp384-kyber-768r3-sha384-d00@openquantumsafe.org", + ); + } + + #[test] + fn ecdh_nistp521_kyber_1024r3_sha512_d00_openquantumsafe_org_test() { + test_alg_with_directory( + "ecdh-nistp521-kyber-1024r3-sha512-d00", + "ecdh-nistp521-kyber-1024r3-sha512-d00@openquantumsafe.org", + ); + } +} + mod kex_algorithm_negociation { use super::ssh_kex_negociate_algorithm;