aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
-rw-r--r--oxish-aws-lc/src/lib.rs47
-rw-r--r--oxish-graviola/src/lib.rs40
-rw-r--r--oxish-proto/src/key_exchange.rs11
-rw-r--r--oxish-proto/src/named.rs6
-rw-r--r--oxish/src/tests.rs31
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() {