Skip to content

Commit 7e71bb4

Browse files
scott-xuRob-Hague
andauthored
Add support for mlkem768x25519-sha256 key exchange method (#1563)
Co-authored-by: Rob Hague <[email protected]>
1 parent 9e1ee0a commit 7e71bb4

13 files changed

+351
-6
lines changed

README.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,9 @@ The main types provided by this library are:
8282
## Key Exchange Methods
8383

8484
**SSH.NET** supports the following key exchange methods:
85+
* mlkem768x25519-sha256
86+
* sntrup761x25519-sha512
87+
* sntrup761x25519-sha512<span></span>@openssh.com
8588
* curve25519-sha256
8689
* curve25519-sha256<span></span>@libssh.org
8790
* ecdh-sha2-nistp256

src/Renci.SshNet/ConnectionInfo.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -349,6 +349,7 @@ public ConnectionInfo(string host, int port, string username, ProxyTypes proxyTy
349349

350350
KeyExchangeAlgorithms = new Dictionary<string, Func<IKeyExchange>>
351351
{
352+
{ "mlkem768x25519-sha256", () => new KeyExchangeMLKem768X25519Sha256() },
352353
{ "sntrup761x25519-sha512", () => new KeyExchangeSNtruP761X25519Sha512() },
353354
{ "[email protected]", () => new KeyExchangeSNtruP761X25519Sha512() },
354355
{ "curve25519-sha256", () => new KeyExchangeECCurve25519() },

src/Renci.SshNet/Messages/Transport/KeyExchangeEcdhInitMessage.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
namespace Renci.SshNet.Messages.Transport
44
{
55
/// <summary>
6-
/// Represents SSH_MSG_KEXECDH_INIT message.
6+
/// Represents SSH_MSG_KEX_ECDH_INIT message.
77
/// </summary>
88
internal sealed class KeyExchangeEcdhInitMessage : Message, IKeyExchangedAllowed
99
{

src/Renci.SshNet/Messages/Transport/KeyExchangeEcdhReplyMessage.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
namespace Renci.SshNet.Messages.Transport
22
{
33
/// <summary>
4-
/// Represents SSH_MSG_KEXECDH_REPLY message.
4+
/// Represents SSH_MSG_KEX_ECDH_REPLY message.
55
/// </summary>
66
public class KeyExchangeEcdhReplyMessage : Message
77
{
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
using System;
2+
3+
namespace Renci.SshNet.Messages.Transport
4+
{
5+
/// <summary>
6+
/// Represents SSH_MSG_KEX_HYBRID_INIT message.
7+
/// </summary>
8+
internal sealed class KeyExchangeHybridInitMessage : Message, IKeyExchangedAllowed
9+
{
10+
/// <inheritdoc />
11+
public override string MessageName
12+
{
13+
get
14+
{
15+
return "SSH_MSG_KEX_HYBRID_INIT";
16+
}
17+
}
18+
19+
/// <inheritdoc />
20+
public override byte MessageNumber
21+
{
22+
get
23+
{
24+
return 30;
25+
}
26+
}
27+
28+
/// <summary>
29+
/// Gets the client init data.
30+
/// </summary>
31+
/// <remarks>
32+
/// The init data is the concatenation of C_PK2 and C_PK1 (C_INIT = C_PK2 || C_PK1, where || depicts concatenation).
33+
/// C_PK1 and C_PK2 represent the ephemeral client public keys used for each key exchange of the PQ/T Hybrid mechanism.
34+
/// Typically, C_PK1 represents a traditional / classical (i.e., ECDH) key exchange public key.
35+
/// C_PK2 represents the 'pk' output of the corresponding post-quantum KEM's 'KeyGen' at the client.
36+
/// </remarks>
37+
public byte[] CInit { get; private set; }
38+
39+
/// <summary>
40+
/// Gets the size of the message in bytes.
41+
/// </summary>
42+
/// <value>
43+
/// The size of the messages in bytes.
44+
/// </value>
45+
protected override int BufferCapacity
46+
{
47+
get
48+
{
49+
var capacity = base.BufferCapacity;
50+
capacity += 4; // CInit length
51+
capacity += CInit.Length; // CInit
52+
return capacity;
53+
}
54+
}
55+
56+
/// <summary>
57+
/// Initializes a new instance of the <see cref="KeyExchangeHybridInitMessage"/> class.
58+
/// </summary>
59+
public KeyExchangeHybridInitMessage(byte[] init)
60+
{
61+
CInit = init;
62+
}
63+
64+
/// <summary>
65+
/// Called when type specific data need to be loaded.
66+
/// </summary>
67+
protected override void LoadData()
68+
{
69+
CInit = ReadBinary();
70+
}
71+
72+
/// <summary>
73+
/// Called when type specific data need to be saved.
74+
/// </summary>
75+
protected override void SaveData()
76+
{
77+
WriteBinaryString(CInit);
78+
}
79+
80+
internal override void Process(Session session)
81+
{
82+
throw new NotImplementedException();
83+
}
84+
}
85+
}
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
namespace Renci.SshNet.Messages.Transport
2+
{
3+
/// <summary>
4+
/// Represents SSH_MSG_KEX_HYBRID_REPLY message.
5+
/// </summary>
6+
public class KeyExchangeHybridReplyMessage : Message
7+
{
8+
/// <inheritdoc />
9+
public override string MessageName
10+
{
11+
get
12+
{
13+
return "SSH_MSG_KEX_HYBRID_REPLY";
14+
}
15+
}
16+
17+
/// <inheritdoc />
18+
public override byte MessageNumber
19+
{
20+
get
21+
{
22+
return 31;
23+
}
24+
}
25+
26+
/// <summary>
27+
/// Gets a string encoding an X.509v3 certificate containing the server's ECDSA public host key.
28+
/// </summary>
29+
/// <value>The host key.</value>
30+
public byte[] KS { get; private set; }
31+
32+
/// <summary>
33+
/// Gets the server reply.
34+
/// </summary>
35+
/// <remarks>
36+
/// The server reply is the concatenation of S_CT2 and S_PK1 (S_REPLY = S_CT2 || S_PK1).
37+
/// Typically, S_PK1 represents the ephemeral (EC)DH server public key.
38+
/// S_CT2 represents the ciphertext 'ct' output of the corresponding KEM's 'Encaps' algorithm generated by
39+
/// the server which encapsulates a secret to the client public key C_PK2.
40+
/// </remarks>
41+
public byte[] SReply { get; private set; }
42+
43+
/// <summary>
44+
/// Gets an octet string containing the server's signature of the newly established exchange hash value.
45+
/// </summary>
46+
/// <value>The signature.</value>
47+
public byte[] Signature { get; private set; }
48+
49+
/// <summary>
50+
/// Gets the size of the message in bytes.
51+
/// </summary>
52+
/// <value>
53+
/// The size of the messages in bytes.
54+
/// </value>
55+
protected override int BufferCapacity
56+
{
57+
get
58+
{
59+
var capacity = base.BufferCapacity;
60+
capacity += 4; // KS length
61+
capacity += KS.Length; // KS
62+
capacity += 4; // SReply length
63+
capacity += SReply.Length; // SReply
64+
capacity += 4; // Signature length
65+
capacity += Signature.Length; // Signature
66+
return capacity;
67+
}
68+
}
69+
70+
/// <summary>
71+
/// Called when type specific data need to be loaded.
72+
/// </summary>
73+
protected override void LoadData()
74+
{
75+
KS = ReadBinary();
76+
SReply = ReadBinary();
77+
Signature = ReadBinary();
78+
}
79+
80+
/// <summary>
81+
/// Called when type specific data need to be saved.
82+
/// </summary>
83+
protected override void SaveData()
84+
{
85+
WriteBinaryString(KS);
86+
WriteBinaryString(SReply);
87+
WriteBinaryString(Signature);
88+
}
89+
90+
internal override void Process(Session session)
91+
{
92+
session.OnKeyExchangeHybridReplyMessageReceived(this);
93+
}
94+
}
95+
}

src/Renci.SshNet/Security/KeyExchangeECCurve25519.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,7 @@ private void Session_KeyExchangeEcdhReplyMessageReceived(object sender, MessageE
8282

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

85-
// When SSH_MSG_KEXDH_REPLY received key exchange is completed
85+
// When SSH_MSG_KEX_ECDH_REPLY received key exchange is completed
8686
Finish();
8787
}
8888

src/Renci.SshNet/Security/KeyExchangeECDH.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@ private void Session_KeyExchangeEcdhReplyMessageReceived(object sender, MessageE
7575

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

78-
// When SSH_MSG_KEXDH_REPLY received key exchange is completed
78+
// When SSH_MSG_KEX_ECDH_REPLY received key exchange is completed
7979
Finish();
8080
}
8181

Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
using System.Globalization;
2+
using System.Linq;
3+
4+
using Org.BouncyCastle.Crypto.Agreement;
5+
using Org.BouncyCastle.Crypto.Generators;
6+
using Org.BouncyCastle.Crypto.Kems;
7+
using Org.BouncyCastle.Crypto.Parameters;
8+
9+
using Renci.SshNet.Abstractions;
10+
using Renci.SshNet.Common;
11+
using Renci.SshNet.Messages.Transport;
12+
13+
namespace Renci.SshNet.Security
14+
{
15+
internal sealed class KeyExchangeMLKem768X25519Sha256 : KeyExchangeEC
16+
{
17+
private MLKemDecapsulator _mlkemDecapsulator;
18+
private X25519Agreement _x25519Agreement;
19+
20+
/// <summary>
21+
/// Gets algorithm name.
22+
/// </summary>
23+
public override string Name
24+
{
25+
get { return "mlkem768x25519-sha256"; }
26+
}
27+
28+
/// <summary>
29+
/// Gets the size, in bits, of the computed hash code.
30+
/// </summary>
31+
/// <value>
32+
/// The size, in bits, of the computed hash code.
33+
/// </value>
34+
protected override int HashSize
35+
{
36+
get { return 256; }
37+
}
38+
39+
/// <inheritdoc/>
40+
public override void Start(Session session, KeyExchangeInitMessage message, bool sendClientInitMessage)
41+
{
42+
base.Start(session, message, sendClientInitMessage);
43+
44+
Session.RegisterMessage("SSH_MSG_KEX_HYBRID_REPLY");
45+
46+
Session.KeyExchangeHybridReplyMessageReceived += Session_KeyExchangeHybridReplyMessageReceived;
47+
48+
var mlkem768KeyPairGenerator = new MLKemKeyPairGenerator();
49+
mlkem768KeyPairGenerator.Init(new MLKemKeyGenerationParameters(CryptoAbstraction.SecureRandom, MLKemParameters.ml_kem_768));
50+
var mlkem768KeyPair = mlkem768KeyPairGenerator.GenerateKeyPair();
51+
52+
_mlkemDecapsulator = new MLKemDecapsulator(MLKemParameters.ml_kem_768);
53+
_mlkemDecapsulator.Init(mlkem768KeyPair.Private);
54+
55+
var x25519KeyPairGenerator = new X25519KeyPairGenerator();
56+
x25519KeyPairGenerator.Init(new X25519KeyGenerationParameters(CryptoAbstraction.SecureRandom));
57+
var x25519KeyPair = x25519KeyPairGenerator.GenerateKeyPair();
58+
59+
_x25519Agreement = new X25519Agreement();
60+
_x25519Agreement.Init(x25519KeyPair.Private);
61+
62+
var mlkem768PublicKey = ((MLKemPublicKeyParameters)mlkem768KeyPair.Public).GetEncoded();
63+
var x25519PublicKey = ((X25519PublicKeyParameters)x25519KeyPair.Public).GetEncoded();
64+
65+
_clientExchangeValue = mlkem768PublicKey.Concat(x25519PublicKey);
66+
67+
SendMessage(new KeyExchangeHybridInitMessage(_clientExchangeValue));
68+
}
69+
70+
/// <summary>
71+
/// Finishes key exchange algorithm.
72+
/// </summary>
73+
public override void Finish()
74+
{
75+
base.Finish();
76+
77+
Session.KeyExchangeHybridReplyMessageReceived -= Session_KeyExchangeHybridReplyMessageReceived;
78+
}
79+
80+
/// <summary>
81+
/// Hashes the specified data bytes.
82+
/// </summary>
83+
/// <param name="hashData">The hash data.</param>
84+
/// <returns>
85+
/// The hash of the data.
86+
/// </returns>
87+
protected override byte[] Hash(byte[] hashData)
88+
{
89+
return CryptoAbstraction.HashSHA256(hashData);
90+
}
91+
92+
private void Session_KeyExchangeHybridReplyMessageReceived(object sender, MessageEventArgs<KeyExchangeHybridReplyMessage> e)
93+
{
94+
var message = e.Message;
95+
96+
// Unregister message once received
97+
Session.UnRegisterMessage("SSH_MSG_KEX_HYBRID_REPLY");
98+
99+
HandleServerHybridReply(message.KS, message.SReply, message.Signature);
100+
101+
// When SSH_MSG_KEX_HYBRID_REPLY received key exchange is completed
102+
Finish();
103+
}
104+
105+
/// <summary>
106+
/// Handles the server hybrid reply message.
107+
/// </summary>
108+
/// <param name="hostKey">The host key.</param>
109+
/// <param name="serverExchangeValue">The server exchange value.</param>
110+
/// <param name="signature">The signature.</param>
111+
private void HandleServerHybridReply(byte[] hostKey, byte[] serverExchangeValue, byte[] signature)
112+
{
113+
_serverExchangeValue = serverExchangeValue;
114+
_hostKey = hostKey;
115+
_signature = signature;
116+
117+
if (serverExchangeValue.Length != _mlkemDecapsulator.EncapsulationLength + _x25519Agreement.AgreementSize)
118+
{
119+
throw new SshConnectionException(
120+
string.Format(CultureInfo.CurrentCulture, "Bad S_Reply length: {0}.", serverExchangeValue.Length),
121+
DisconnectReason.KeyExchangeFailed);
122+
}
123+
124+
var secret = new byte[_mlkemDecapsulator.SecretLength + _x25519Agreement.AgreementSize];
125+
126+
_mlkemDecapsulator.Decapsulate(serverExchangeValue, 0, _mlkemDecapsulator.EncapsulationLength, secret, 0, _mlkemDecapsulator.SecretLength);
127+
128+
var x25519PublicKey = new X25519PublicKeyParameters(serverExchangeValue, _mlkemDecapsulator.EncapsulationLength);
129+
_x25519Agreement.CalculateAgreement(x25519PublicKey, secret, _mlkemDecapsulator.SecretLength);
130+
131+
SharedKey = CryptoAbstraction.HashSHA256(secret);
132+
}
133+
}
134+
}

src/Renci.SshNet/Session.cs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -447,6 +447,11 @@ public string ClientVersion
447447
/// </summary>
448448
internal event EventHandler<MessageEventArgs<KeyExchangeEcdhReplyMessage>> KeyExchangeEcdhReplyMessageReceived;
449449

450+
/// <summary>
451+
/// Occurs when a <see cref="KeyExchangeHybridReplyMessage"/> message is received from the SSH server.
452+
/// </summary>
453+
internal event EventHandler<MessageEventArgs<KeyExchangeHybridReplyMessage>> KeyExchangeHybridReplyMessageReceived;
454+
450455
/// <summary>
451456
/// Occurs when <see cref="NewKeysMessage"/> message received
452457
/// </summary>
@@ -1535,6 +1540,11 @@ internal void OnKeyExchangeEcdhReplyMessageReceived(KeyExchangeEcdhReplyMessage
15351540
KeyExchangeEcdhReplyMessageReceived?.Invoke(this, new MessageEventArgs<KeyExchangeEcdhReplyMessage>(message));
15361541
}
15371542

1543+
internal void OnKeyExchangeHybridReplyMessageReceived(KeyExchangeHybridReplyMessage message)
1544+
{
1545+
KeyExchangeHybridReplyMessageReceived?.Invoke(this, new MessageEventArgs<KeyExchangeHybridReplyMessage>(message));
1546+
}
1547+
15381548
/// <summary>
15391549
/// Called when <see cref="NewKeysMessage"/> message received.
15401550
/// </summary>

0 commit comments

Comments
 (0)