Skip to content

Commit

Permalink
Add support for mlkem768x25519-sha256 key exchange method
Browse files Browse the repository at this point in the history
  • Loading branch information
scott-xu committed Dec 27, 2024
1 parent 29997ae commit 235632d
Show file tree
Hide file tree
Showing 12 changed files with 349 additions and 7 deletions.
1 change: 1 addition & 0 deletions src/Renci.SshNet/ConnectionInfo.cs
Original file line number Diff line number Diff line change
Expand Up @@ -348,6 +348,7 @@ public ConnectionInfo(string host, int port, string username, ProxyTypes proxyTy

KeyExchangeAlgorithms = new Dictionary<string, Func<IKeyExchange>>
{
{ "mlkem768x25519-sha256", () => new KeyExchangeMLKem768x25519Sha256() },
{ "curve25519-sha256", () => new KeyExchangeECCurve25519() },
{ "[email protected]", () => new KeyExchangeECCurve25519() },
{ "ecdh-sha2-nistp256", () => new KeyExchangeECDH256() },
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 = serverExchangeValue.Take(_mlkemDecapsulator.EncapsulationLength, _x25519Agreement.AgreementSize);
_x25519Agreement.CalculateAgreement(new X25519PublicKeyParameters(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

0 comments on commit 235632d

Please sign in to comment.