diff options
| -rw-r--r-- | oxish-aws-lc/src/lib.rs | 47 | ||||
| -rw-r--r-- | oxish-graviola/src/lib.rs | 40 | ||||
| -rw-r--r-- | oxish-proto/src/key_exchange.rs | 11 | ||||
| -rw-r--r-- | oxish-proto/src/named.rs | 6 | ||||
| -rw-r--r-- | oxish/src/tests.rs | 31 |
5 files changed, 129 insertions, 6 deletions
diff --git a/oxish-aws-lc/src/lib.rs b/oxish-aws-lc/src/lib.rs index f63f2d8..f7e95cc 100644 --- a/oxish-aws-lc/src/lib.rs +++ b/oxish-aws-lc/src/lib.rs @@ -118,20 +118,26 @@ impl CryptoProvider for Provider { ) -> Result<&'static dyn KeyExchange, CryptoError> { match algorithm { KeyExchangeAlgorithm::MlKem768X25519Sha256 => Ok(&Mlkem768X25519Kx), + KeyExchangeAlgorithm::Curve25519Sha256 => Ok(&X25519Kx), _ => Err(CryptoError::UnknownAlgorithm), } } fn hash(&self, algorithm: &KeyExchangeAlgorithm<'_>) -> Result<&'static dyn Hash, CryptoError> { match algorithm { - KeyExchangeAlgorithm::MlKem768X25519Sha256 => Ok(&Sha256), + KeyExchangeAlgorithm::MlKem768X25519Sha256 | KeyExchangeAlgorithm::Curve25519Sha256 => { + Ok(&Sha256) + } _ => Err(CryptoError::UnknownAlgorithm), } } fn supported_algorithms(&self) -> SupportedAlgorithms { SupportedAlgorithms { - key_exchange: &[KeyExchangeAlgorithm::MlKem768X25519Sha256], + key_exchange: &[ + KeyExchangeAlgorithm::MlKem768X25519Sha256, + KeyExchangeAlgorithm::Curve25519Sha256, + ], public_key: &[ PublicKeyAlgorithm::EcdsaSha2Nistp256, PublicKeyAlgorithm::Ed25519, @@ -334,6 +340,43 @@ impl ActiveKeyExchange for Mlkem768X25519KeyExchange { } } +/// The `curve25519-sha256` key exchange +struct X25519Kx; + +impl KeyExchange for X25519Kx { + fn start(&self) -> Result<Box<dyn ActiveKeyExchange>, CryptoError> { + Ok(Box::new(X25519KeyExchange)) + } +} + +struct X25519KeyExchange; + +impl ActiveKeyExchange for X25519KeyExchange { + fn complete(self: Box<Self>, peer_public_key: &[u8]) -> Result<AgreedKey, CryptoError> { + let random = rand::SystemRandom::new(); + let private_key = EphemeralPrivateKey::generate(&X25519, &random) + .map_err(|_| CryptoError::KeyGenerationFailed)?; + let public_key = private_key + .compute_public_key() + .map_err(|_| CryptoError::Unspecified)? + .as_ref() + .to_vec(); + + let peer = agreement::UnparsedPublicKey::new(&X25519, peer_public_key); + let shared_secret = agreement::agree_ephemeral( + private_key, + peer, + CryptoError::KeyAgreementFailed, + |shared_secret| Ok(SharedSecret::from(shared_secret.to_vec())), + )?; + + Ok(AgreedKey { + public_key, + shared_secret, + }) + } +} + struct Ed25519SigningKey { key_pair: Ed25519KeyPair, public_key: Vec<u8>, diff --git a/oxish-graviola/src/lib.rs b/oxish-graviola/src/lib.rs index e170ea9..7cccdbf 100644 --- a/oxish-graviola/src/lib.rs +++ b/oxish-graviola/src/lib.rs @@ -126,20 +126,26 @@ impl CryptoProvider for Provider { ) -> Result<&'static dyn KeyExchange, CryptoError> { match algorithm { KeyExchangeAlgorithm::MlKem768X25519Sha256 => Ok(&Mlkem768X25519Kx), + KeyExchangeAlgorithm::Curve25519Sha256 => Ok(&X25519Kx), _ => Err(CryptoError::UnknownAlgorithm), } } fn hash(&self, algorithm: &KeyExchangeAlgorithm<'_>) -> Result<&'static dyn Hash, CryptoError> { match algorithm { - KeyExchangeAlgorithm::MlKem768X25519Sha256 => Ok(&Sha256), + KeyExchangeAlgorithm::MlKem768X25519Sha256 | KeyExchangeAlgorithm::Curve25519Sha256 => { + Ok(&Sha256) + } _ => Err(CryptoError::UnknownAlgorithm), } } fn supported_algorithms(&self) -> SupportedAlgorithms { SupportedAlgorithms { - key_exchange: &[KeyExchangeAlgorithm::MlKem768X25519Sha256], + key_exchange: &[ + KeyExchangeAlgorithm::MlKem768X25519Sha256, + KeyExchangeAlgorithm::Curve25519Sha256, + ], public_key: &[ PublicKeyAlgorithm::EcdsaSha2Nistp256, PublicKeyAlgorithm::Ed25519, @@ -325,6 +331,36 @@ impl ActiveKeyExchange for Mlkem768X25519KeyExchange { } } +/// The `curve25519-sha256` key exchange +struct X25519Kx; + +impl KeyExchange for X25519Kx { + fn start(&self) -> Result<Box<dyn ActiveKeyExchange>, CryptoError> { + Ok(Box::new(X25519KeyExchange)) + } +} + +struct X25519KeyExchange; + +impl ActiveKeyExchange for X25519KeyExchange { + fn complete(self: Box<Self>, peer_public_key: &[u8]) -> Result<AgreedKey, CryptoError> { + let private_key = + x25519::PrivateKey::new_random().map_err(|_| CryptoError::NoRandomness)?; + let public_key = private_key.public_key().as_bytes().to_vec(); + + let peer = x25519::PublicKey::try_from_slice(peer_public_key) + .map_err(|_| CryptoError::KeyRejected)?; + let shared = private_key + .diffie_hellman(&peer) + .map_err(|_| CryptoError::KeyAgreementFailed)?; + + Ok(AgreedKey { + public_key, + shared_secret: SharedSecret::from(shared.as_bytes().to_vec()), + }) + } +} + struct Ed25519Key { key: Ed25519SigningKey, public_key: Vec<u8>, diff --git a/oxish-proto/src/key_exchange.rs b/oxish-proto/src/key_exchange.rs index cbb8e2e..898fb46 100644 --- a/oxish-proto/src/key_exchange.rs +++ b/oxish-proto/src/key_exchange.rs @@ -614,8 +614,15 @@ impl KeyExchangeOutput { // Write the server's reply public value (`Q_S` / `S_REPLY`) to the exchange hash exchange.prefixed(&completed.public_key); let secret_bytes = completed.shared_secret.secret_bytes(); - let mut shared_secret = Vec::with_capacity(secret_bytes.len() + 4); - secret_bytes.encode(&mut shared_secret); + let mut shared_secret = Vec::with_capacity(secret_bytes.len() + 5); + match negotiated.key_exchange { + // RFC 8731 section 3: `K` is the raw X25519 output encoded as an mpint + KeyExchangeAlgorithm::Curve25519Sha256 => { + encode_mpint(secret_bytes, &mut shared_secret) + } + // The PQ hybrid draft encodes `K` (a fixed-length hash output) as a string + _ => secret_bytes.encode(&mut shared_secret), + } exchange.update(&shared_secret); let exchange_hash = exchange.finish(); diff --git a/oxish-proto/src/named.rs b/oxish-proto/src/named.rs index bc6dbea..4b24417 100644 --- a/oxish-proto/src/named.rs +++ b/oxish-proto/src/named.rs @@ -180,6 +180,10 @@ pub enum KeyExchangeAlgorithm<'a> { /// /// As defined in <https://datatracker.ietf.org/doc/draft-kampanakis-curdle-ssh-pq-ke/>. MlKem768X25519Sha256, + /// `curve25519-sha256` key exchange algorithm: ECDH using X25519 + /// + /// As defined in <https://www.rfc-editor.org/rfc/rfc8731>. + Curve25519Sha256, /// A key exchange algorithm not known to this implementation Unknown(&'a str), } @@ -188,6 +192,7 @@ impl<'a> Named<'a> for KeyExchangeAlgorithm<'a> { fn typed(name: &'a str) -> Self { match name { "mlkem768x25519-sha256" => Self::MlKem768X25519Sha256, + "curve25519-sha256" => Self::Curve25519Sha256, _ => Self::Unknown(name), } } @@ -195,6 +200,7 @@ impl<'a> Named<'a> for KeyExchangeAlgorithm<'a> { fn name(&self) -> &str { match self { Self::MlKem768X25519Sha256 => "mlkem768x25519-sha256", + Self::Curve25519Sha256 => "curve25519-sha256", Self::Unknown(name) => name, } } diff --git a/oxish/src/tests.rs b/oxish/src/tests.rs index f1d8f5e..0f4abc7 100644 --- a/oxish/src/tests.rs +++ b/oxish/src/tests.rs @@ -63,6 +63,37 @@ async fn handshake_ed25519_graviola() { .unwrap(); } +/// Exercise the curve25519-sha256 key exchange against the aws-lc-rs provider +#[cfg(feature = "aws-lc")] +#[tokio::test] +async fn handshake_x25519_aws_lc() { + handshake_x25519(aws_lc::DEFAULT_PROVIDER).await.unwrap(); +} + +/// Exercise the curve25519-sha256 key exchange against the graviola provider +#[cfg(feature = "graviola")] +#[tokio::test] +async fn handshake_x25519_graviola() { + handshake_x25519(graviola::DEFAULT_PROVIDER).await.unwrap(); +} + +async fn handshake_x25519(provider: &'static dyn CryptoProvider) -> anyhow::Result<()> { + subscribe(); + + let (_key_dir, mut client, server) = + setup(&PublicKeyAlgorithm::Ed25519, None, provider).await?; + // Restrict the client to the non-PQ key exchange so the test fails if the + // server no longer supports it. + client.cmd.args(["-o", "KexAlgorithms=curve25519-sha256"]); + + let (_stdout, stderr) = client.run(COMMAND, Duration::from_secs(10), server).await?; + anyhow::ensure!( + stderr.contains("kex: algorithm: curve25519-sha256"), + "client did not negotiate curve25519-sha256" + ); + Ok(()) +} + #[cfg(feature = "graviola")] #[tokio::test] async fn no_spawn() { |
