Skip to content

[PQC] Add support for mlkem768x25519-sha256 key exchange method #1563

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Jan 11, 2025
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
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,9 @@ The main types provided by this library are:
## Key Exchange Methods

**SSH.NET** supports the following key exchange methods:
* mlkem768x25519-sha256
* sntrup761x25519-sha512
* sntrup761x25519-sha512<span></span>@openssh.com
* curve25519-sha256
* curve25519-sha256<span></span>@libssh.org
* ecdh-sha2-nistp256
Expand Down
1 change: 1 addition & 0 deletions src/Renci.SshNet/ConnectionInfo.cs
Original file line number Diff line number Diff line change
Expand Up @@ -349,6 +349,7 @@ public ConnectionInfo(string host, int port, string username, ProxyTypes proxyTy

KeyExchangeAlgorithms = new Dictionary<string, Func<IKeyExchange>>
{
{ "mlkem768x25519-sha256", () => new KeyExchangeMLKem768X25519Sha256() },
{ "sntrup761x25519-sha512", () => new KeyExchangeSNtruP761X25519Sha512() },
{ "sntrup761x25519-sha512@openssh.com", () => new KeyExchangeSNtruP761X25519Sha512() },
{ "curve25519-sha256", () => new KeyExchangeECCurve25519() },
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
namespace Renci.SshNet.Messages.Transport
{
/// <summary>
/// Represents SSH_MSG_KEXECDH_INIT message.
/// Represents SSH_MSG_KEX_ECDH_INIT message.
/// </summary>
internal sealed class KeyExchangeEcdhInitMessage : Message, IKeyExchangedAllowed
{
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
namespace Renci.SshNet.Messages.Transport
{
/// <summary>
/// Represents SSH_MSG_KEXECDH_REPLY message.
/// Represents SSH_MSG_KEX_ECDH_REPLY message.
/// </summary>
public class KeyExchangeEcdhReplyMessage : Message
{
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
using System;

namespace Renci.SshNet.Messages.Transport
{
/// <summary>
/// Represents SSH_MSG_KEX_HYBRID_INIT message.
/// </summary>
internal sealed class KeyExchangeHybridInitMessage : Message, IKeyExchangedAllowed
{
/// <inheritdoc />
public override string MessageName
{
get
{
return "SSH_MSG_KEX_HYBRID_INIT";
}
}

/// <inheritdoc />
public override byte MessageNumber
{
get
{
return 30;
}
}

/// <summary>
/// Gets the client init data.
/// </summary>
/// <remarks>
/// The init data is the concatenation of C_PK2 and C_PK1 (C_INIT = C_PK2 || C_PK1, where || depicts concatenation).
/// C_PK1 and C_PK2 represent the ephemeral client public keys used for each key exchange of the PQ/T Hybrid mechanism.
/// Typically, C_PK1 represents a traditional / classical (i.e., ECDH) key exchange public key.
/// C_PK2 represents the 'pk' output of the corresponding post-quantum KEM's 'KeyGen' at the client.
/// </remarks>
public byte[] CInit { get; private set; }

/// <summary>
/// Gets the size of the message in bytes.
/// </summary>
/// <value>
/// The size of the messages in bytes.
/// </value>
protected override int BufferCapacity
{
get
{
var capacity = base.BufferCapacity;
capacity += 4; // CInit length
capacity += CInit.Length; // CInit
return capacity;
}
}

/// <summary>
/// Initializes a new instance of the <see cref="KeyExchangeHybridInitMessage"/> class.
/// </summary>
public KeyExchangeHybridInitMessage(byte[] init)
{
CInit = init;
}

/// <summary>
/// Called when type specific data need to be loaded.
/// </summary>
protected override void LoadData()
{
CInit = ReadBinary();
}

/// <summary>
/// Called when type specific data need to be saved.
/// </summary>
protected override void SaveData()
{
WriteBinaryString(CInit);
}

internal override void Process(Session session)
{
throw new NotImplementedException();
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
namespace Renci.SshNet.Messages.Transport
{
/// <summary>
/// Represents SSH_MSG_KEX_HYBRID_REPLY message.
/// </summary>
public class KeyExchangeHybridReplyMessage : Message
{
/// <inheritdoc />
public override string MessageName
{
get
{
return "SSH_MSG_KEX_HYBRID_REPLY";
}
}

/// <inheritdoc />
public override byte MessageNumber
{
get
{
return 31;
}
}

/// <summary>
/// Gets a string encoding an X.509v3 certificate containing the server's ECDSA public host key.
/// </summary>
/// <value>The host key.</value>
public byte[] KS { get; private set; }

/// <summary>
/// Gets the server reply.
/// </summary>
/// <remarks>
/// The server reply is the concatenation of S_CT2 and S_PK1 (S_REPLY = S_CT2 || S_PK1).
/// Typically, S_PK1 represents the ephemeral (EC)DH server public key.
/// S_CT2 represents the ciphertext 'ct' output of the corresponding KEM's 'Encaps' algorithm generated by
/// the server which encapsulates a secret to the client public key C_PK2.
/// </remarks>
public byte[] SReply { get; private set; }

/// <summary>
/// Gets an octet string containing the server's signature of the newly established exchange hash value.
/// </summary>
/// <value>The signature.</value>
public byte[] Signature { get; private set; }

/// <summary>
/// Gets the size of the message in bytes.
/// </summary>
/// <value>
/// The size of the messages in bytes.
/// </value>
protected override int BufferCapacity
{
get
{
var capacity = base.BufferCapacity;
capacity += 4; // KS length
capacity += KS.Length; // KS
capacity += 4; // SReply length
capacity += SReply.Length; // SReply
capacity += 4; // Signature length
capacity += Signature.Length; // Signature
return capacity;
}
}

/// <summary>
/// Called when type specific data need to be loaded.
/// </summary>
protected override void LoadData()
{
KS = ReadBinary();
SReply = ReadBinary();
Signature = ReadBinary();
}

/// <summary>
/// Called when type specific data need to be saved.
/// </summary>
protected override void SaveData()
{
WriteBinaryString(KS);
WriteBinaryString(SReply);
WriteBinaryString(Signature);
}

internal override void Process(Session session)
{
session.OnKeyExchangeHybridReplyMessageReceived(this);
}
}
}
2 changes: 1 addition & 1 deletion src/Renci.SshNet/Security/KeyExchangeECCurve25519.cs
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ private void Session_KeyExchangeEcdhReplyMessageReceived(object sender, MessageE

HandleServerEcdhReply(message.KS, message.QS, message.Signature);

// When SSH_MSG_KEXDH_REPLY received key exchange is completed
// When SSH_MSG_KEX_ECDH_REPLY received key exchange is completed
Finish();
}

Expand Down
2 changes: 1 addition & 1 deletion src/Renci.SshNet/Security/KeyExchangeECDH.cs
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ private void Session_KeyExchangeEcdhReplyMessageReceived(object sender, MessageE

HandleServerEcdhReply(message.KS, message.QS, message.Signature);

// When SSH_MSG_KEXDH_REPLY received key exchange is completed
// When SSH_MSG_KEX_ECDH_REPLY received key exchange is completed
Finish();
}

Expand Down
134 changes: 134 additions & 0 deletions src/Renci.SshNet/Security/KeyExchangeMLKem768X25519Sha256.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
using System.Globalization;
using System.Linq;

using Org.BouncyCastle.Crypto.Agreement;
using Org.BouncyCastle.Crypto.Generators;
using Org.BouncyCastle.Crypto.Kems;
using Org.BouncyCastle.Crypto.Parameters;

using Renci.SshNet.Abstractions;
using Renci.SshNet.Common;
using Renci.SshNet.Messages.Transport;

namespace Renci.SshNet.Security
{
internal sealed class KeyExchangeMLKem768X25519Sha256 : KeyExchangeEC
{
private MLKemDecapsulator _mlkemDecapsulator;
private X25519Agreement _x25519Agreement;

/// <summary>
/// Gets algorithm name.
/// </summary>
public override string Name
{
get { return "mlkem768x25519-sha256"; }
}

/// <summary>
/// Gets the size, in bits, of the computed hash code.
/// </summary>
/// <value>
/// The size, in bits, of the computed hash code.
/// </value>
protected override int HashSize
{
get { return 256; }
}

/// <inheritdoc/>
public override void Start(Session session, KeyExchangeInitMessage message, bool sendClientInitMessage)
{
base.Start(session, message, sendClientInitMessage);

Session.RegisterMessage("SSH_MSG_KEX_HYBRID_REPLY");

Session.KeyExchangeHybridReplyMessageReceived += Session_KeyExchangeHybridReplyMessageReceived;

var mlkem768KeyPairGenerator = new MLKemKeyPairGenerator();
mlkem768KeyPairGenerator.Init(new MLKemKeyGenerationParameters(CryptoAbstraction.SecureRandom, MLKemParameters.ml_kem_768));
var mlkem768KeyPair = mlkem768KeyPairGenerator.GenerateKeyPair();

_mlkemDecapsulator = new MLKemDecapsulator(MLKemParameters.ml_kem_768);
_mlkemDecapsulator.Init(mlkem768KeyPair.Private);

var x25519KeyPairGenerator = new X25519KeyPairGenerator();
x25519KeyPairGenerator.Init(new X25519KeyGenerationParameters(CryptoAbstraction.SecureRandom));
var x25519KeyPair = x25519KeyPairGenerator.GenerateKeyPair();

_x25519Agreement = new X25519Agreement();
_x25519Agreement.Init(x25519KeyPair.Private);

var mlkem768PublicKey = ((MLKemPublicKeyParameters)mlkem768KeyPair.Public).GetEncoded();
var x25519PublicKey = ((X25519PublicKeyParameters)x25519KeyPair.Public).GetEncoded();

_clientExchangeValue = mlkem768PublicKey.Concat(x25519PublicKey);

SendMessage(new KeyExchangeHybridInitMessage(_clientExchangeValue));
}

/// <summary>
/// Finishes key exchange algorithm.
/// </summary>
public override void Finish()
{
base.Finish();

Session.KeyExchangeHybridReplyMessageReceived -= Session_KeyExchangeHybridReplyMessageReceived;
}

/// <summary>
/// Hashes the specified data bytes.
/// </summary>
/// <param name="hashData">The hash data.</param>
/// <returns>
/// The hash of the data.
/// </returns>
protected override byte[] Hash(byte[] hashData)
{
return CryptoAbstraction.HashSHA256(hashData);
}

private void Session_KeyExchangeHybridReplyMessageReceived(object sender, MessageEventArgs<KeyExchangeHybridReplyMessage> e)
{
var message = e.Message;

// Unregister message once received
Session.UnRegisterMessage("SSH_MSG_KEX_HYBRID_REPLY");

HandleServerHybridReply(message.KS, message.SReply, message.Signature);

// When SSH_MSG_KEX_HYBRID_REPLY received key exchange is completed
Finish();
}

/// <summary>
/// Handles the server hybrid reply message.
/// </summary>
/// <param name="hostKey">The host key.</param>
/// <param name="serverExchangeValue">The server exchange value.</param>
/// <param name="signature">The signature.</param>
private void HandleServerHybridReply(byte[] hostKey, byte[] serverExchangeValue, byte[] signature)
{
_serverExchangeValue = serverExchangeValue;
_hostKey = hostKey;
_signature = signature;

if (serverExchangeValue.Length != _mlkemDecapsulator.EncapsulationLength + _x25519Agreement.AgreementSize)
{
throw new SshConnectionException(
string.Format(CultureInfo.CurrentCulture, "Bad S_Reply length: {0}.", serverExchangeValue.Length),
DisconnectReason.KeyExchangeFailed);
}

var secret = new byte[_mlkemDecapsulator.SecretLength + _x25519Agreement.AgreementSize];

_mlkemDecapsulator.Decapsulate(serverExchangeValue, 0, _mlkemDecapsulator.EncapsulationLength, secret, 0, _mlkemDecapsulator.SecretLength);

var x25519PublicKey = new X25519PublicKeyParameters(serverExchangeValue, _mlkemDecapsulator.EncapsulationLength);
_x25519Agreement.CalculateAgreement(x25519PublicKey, secret, _mlkemDecapsulator.SecretLength);

SharedKey = CryptoAbstraction.HashSHA256(secret);
}
}
}
10 changes: 10 additions & 0 deletions src/Renci.SshNet/Session.cs
Original file line number Diff line number Diff line change
Expand Up @@ -447,6 +447,11 @@ public string ClientVersion
/// </summary>
internal event EventHandler<MessageEventArgs<KeyExchangeEcdhReplyMessage>> KeyExchangeEcdhReplyMessageReceived;

/// <summary>
/// Occurs when a <see cref="KeyExchangeHybridReplyMessage"/> message is received from the SSH server.
/// </summary>
internal event EventHandler<MessageEventArgs<KeyExchangeHybridReplyMessage>> KeyExchangeHybridReplyMessageReceived;

/// <summary>
/// Occurs when <see cref="NewKeysMessage"/> message received
/// </summary>
Expand Down Expand Up @@ -1535,6 +1540,11 @@ internal void OnKeyExchangeEcdhReplyMessageReceived(KeyExchangeEcdhReplyMessage
KeyExchangeEcdhReplyMessageReceived?.Invoke(this, new MessageEventArgs<KeyExchangeEcdhReplyMessage>(message));
}

internal void OnKeyExchangeHybridReplyMessageReceived(KeyExchangeHybridReplyMessage message)
{
KeyExchangeHybridReplyMessageReceived?.Invoke(this, new MessageEventArgs<KeyExchangeHybridReplyMessage>(message));
}

/// <summary>
/// Called when <see cref="NewKeysMessage"/> message received.
/// </summary>
Expand Down
Loading