Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
179 changes: 177 additions & 2 deletions src/kex.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand Down Expand Up @@ -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 {
/// [email protected]
ECDHNistP256Kyber512r3Sha256D00OQS,

/// [email protected]
ECDHNistP384Kyber768r3Sha384D00OQS,

/// [email protected]
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"))]
Expand Down Expand Up @@ -507,6 +549,100 @@ pub struct SshKEXDiffieHellmanKEXGEX<'a> {
pub reply: Option<SshPacketDhKEXGEXReply<'a>>,
}

/// 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<SshPacketHybridKEXInit<'a>>,

/// The reply message, i.e. `SSH_MSG_KEX_HYBRID_REPLY`.
pub reply: Option<SshPacketHybridKEXReply<'a>>,

/// 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> {
Expand Down Expand Up @@ -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<Item = &'b S1>,
Expand Down Expand Up @@ -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> {
Expand Down Expand Up @@ -642,6 +799,15 @@ impl<'a> SshKEX<'a> {
"diffie-hellman-group-exchange-sha1" | "diffie-hellman-group-exchange-sha256" => Ok(
Self::DiffieHellmanKEXGEX(SshKEXDiffieHellmanKEXGEX::default()),
),
"[email protected]" => Ok(Self::HybridKEX(
SshHybridKEX::new(SupportedHybridKEXAlgorithm::ECDHNistP256Kyber512r3Sha256D00OQS),
)),
"[email protected]" => Ok(Self::HybridKEX(
SshHybridKEX::new(SupportedHybridKEXAlgorithm::ECDHNistP384Kyber768r3Sha384D00OQS),
)),
"[email protected]" => Ok(Self::HybridKEX(
SshHybridKEX::new(SupportedHybridKEXAlgorithm::ECDHNistP521Kyber1024r3Sha512D00OQS),
)),
_ => Err(SshKEXError::UnknownProtocol),
}
.map(|kex| (kex, negociated_alg))
Expand Down Expand Up @@ -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),
},
}
}
}
3 changes: 2 additions & 1 deletion src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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::*;
79 changes: 79 additions & 0 deletions tests/tests_kex.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<Path>, expected_algorithm: impl AsRef<str>) {
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",
"[email protected]",
);
}

#[test]
fn ecdh_nistp384_kyber_768r3_sha384_d00_openquantumsafe_org_test() {
test_alg_with_directory(
"ecdh-nistp384-kyber-768r3-sha384-d00",
"[email protected]",
);
}

#[test]
fn ecdh_nistp521_kyber_1024r3_sha512_d00_openquantumsafe_org_test() {
test_alg_with_directory(
"ecdh-nistp521-kyber-1024r3-sha512-d00",
"[email protected]",
);
}
}

mod kex_algorithm_negociation {
use super::ssh_kex_negociate_algorithm;

Expand Down