Skip to content

Commit 00d13b4

Browse files
committed
Use BCL MLKem
1 parent 933613e commit 00d13b4

File tree

5 files changed

+197
-24
lines changed

5 files changed

+197
-24
lines changed

.github/workflows/build.yml

Lines changed: 84 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ jobs:
3535
-p:CoverletOutput=../../coverlet/linux_unit_test_net_9_coverage.xml \
3636
test/Renci.SshNet.Tests/
3737
38-
- name: Run Integration Tests .NET
38+
- name: Run Integration Tests .NET 1
3939
run: |
4040
dotnet test \
4141
-f net9.0 \
@@ -44,7 +44,33 @@ jobs:
4444
--logger GitHubActions \
4545
-p:CollectCoverage=true \
4646
-p:CoverletOutputFormat=cobertura \
47-
-p:CoverletOutput=../../coverlet/linux_integration_test_net_9_coverage.xml \
47+
-p:CoverletOutput=../../coverlet/linux_integration_test_net_9_coverage_1.xml \
48+
test/Renci.SshNet.IntegrationTests/
49+
50+
- name: Run Integration Tests .NET 2
51+
run: |
52+
dotnet test \
53+
-f net9.0 \
54+
--logger "console;verbosity=normal" \
55+
--logger GitHubActions \
56+
--filter "Name=MLKem768X25519Sha256" \
57+
-p:DefineConstants="Test_BCL_MLKem" \
58+
-p:CollectCoverage=true \
59+
-p:CoverletOutputFormat=cobertura \
60+
-p:CoverletOutput=../../coverlet/linux_integration_test_net_9_coverage_2.xml \
61+
test/Renci.SshNet.IntegrationTests/
62+
63+
- name: Run Integration Tests .NET 3
64+
run: |
65+
dotnet test \
66+
-f net9.0 \
67+
--logger "console;verbosity=normal" \
68+
--logger GitHubActions \
69+
--filter "Name=MLKem768X25519Sha256" \
70+
-p:DefineConstants="Test_BouncyCastle_MLKem" \
71+
-p:CollectCoverage=true \
72+
-p:CoverletOutputFormat=cobertura \
73+
-p:CoverletOutput=../../coverlet/linux_integration_test_net_9_coverage_3.xml \
4874
test/Renci.SshNet.IntegrationTests/
4975
5076
- name: Archive Coverlet Results
@@ -128,15 +154,41 @@ jobs:
128154
podman build -t renci-ssh-tests-server-image -f test/Renci.SshNet.IntegrationTests/Dockerfile test/Renci.SshNet.IntegrationTests/
129155
podman run --rm -h renci-ssh-tests-server -d -p 2222:22 renci-ssh-tests-server-image
130156
131-
- name: Run Integration Tests .NET Framework
157+
- name: Run Integration Tests .NET Framework 1
158+
run:
159+
dotnet test `
160+
-f net48 `
161+
--logger "console;verbosity=normal" `
162+
--logger GitHubActions `
163+
-p:CollectCoverage=true `
164+
-p:CoverletOutputFormat=cobertura `
165+
-p:CoverletOutput=..\..\coverlet\windows_integration_test_net_4_8_coverage_1.xml `
166+
test\Renci.SshNet.IntegrationTests\
167+
168+
- name: Run Integration Tests .NET Framework 2
132169
run:
133170
dotnet test `
134171
-f net48 `
135172
--logger "console;verbosity=normal" `
136173
--logger GitHubActions `
174+
--filter "Name=MLKem768X25519Sha256" `
175+
-p:DefineConstants="Test_BCL_MLKem" `
137176
-p:CollectCoverage=true `
138177
-p:CoverletOutputFormat=cobertura `
139-
-p:CoverletOutput=..\..\coverlet\windows_integration_test_net_4_8_coverage.xml `
178+
-p:CoverletOutput=..\..\coverlet\windows_integration_test_net_4_8_coverage_2.xml `
179+
test\Renci.SshNet.IntegrationTests\
180+
181+
- name: Run Integration Tests .NET Framework 3
182+
run:
183+
dotnet test `
184+
-f net48 `
185+
--logger "console;verbosity=normal" `
186+
--logger GitHubActions `
187+
--filter "Name=MLKem768X25519Sha256" `
188+
-p:DefineConstants="Test_BouncyCastle_MLKem" `
189+
-p:CollectCoverage=true `
190+
-p:CoverletOutputFormat=cobertura `
191+
-p:CoverletOutput=..\..\coverlet\windows_integration_test_net_4_8_coverage_3.xml `
140192
test\Renci.SshNet.IntegrationTests\
141193

142194
- name: Archive Coverlet Results
@@ -170,15 +222,41 @@ jobs:
170222
podman build -t renci-ssh-tests-server-image -f test/Renci.SshNet.IntegrationTests/Dockerfile test/Renci.SshNet.IntegrationTests/
171223
podman run --rm -h renci-ssh-tests-server -d -p 2222:22 renci-ssh-tests-server-image
172224
173-
- name: Run Integration Tests .NET
225+
- name: Run Integration Tests .NET 1
226+
run:
227+
dotnet test `
228+
-f net9.0 `
229+
--logger "console;verbosity=normal" `
230+
--logger GitHubActions `
231+
-p:CollectCoverage=true `
232+
-p:CoverletOutputFormat=cobertura `
233+
-p:CoverletOutput=..\..\coverlet\windows_integration_test_net_9_coverage_1.xml `
234+
test\Renci.SshNet.IntegrationTests\
235+
236+
- name: Run Integration Tests .NET 2
237+
run:
238+
dotnet test `
239+
-f net9.0 `
240+
--logger "console;verbosity=normal" `
241+
--logger GitHubActions `
242+
--filter "Name=MLKem768X25519Sha256" `
243+
-p:DefineConstants="Test_BCL_MLKem" `
244+
-p:CollectCoverage=true `
245+
-p:CoverletOutputFormat=cobertura `
246+
-p:CoverletOutput=..\..\coverlet\windows_integration_test_net_9_coverage_2.xml `
247+
test\Renci.SshNet.IntegrationTests\
248+
249+
- name: Run Integration Tests .NET 3
174250
run:
175251
dotnet test `
176252
-f net9.0 `
177253
--logger "console;verbosity=normal" `
178254
--logger GitHubActions `
255+
--filter "Name=MLKem768X25519Sha256" `
256+
-p:DefineConstants="Test_BouncyCastle_MLKem" `
179257
-p:CollectCoverage=true `
180258
-p:CoverletOutputFormat=cobertura `
181-
-p:CoverletOutput=..\..\coverlet\windows_integration_test_net_9_coverage.xml `
259+
-p:CoverletOutput=..\..\coverlet\windows_integration_test_net_9_coverage_3.xml `
182260
test\Renci.SshNet.IntegrationTests\
183261

184262
- name: Archive Coverlet Results

src/Renci.SshNet/Renci.SshNet.csproj

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,10 @@
4040
<IsAotCompatible>true</IsAotCompatible>
4141
</PropertyGroup>
4242

43+
<PropertyGroup Condition="'$(Configuration)|$(TargetFramework)|$(Platform)'=='Debug|net462|AnyCPU'">
44+
<DefineConstants>$(DefineConstants);Test_BCL_MLKem</DefineConstants>
45+
</PropertyGroup>
46+
4347
<ItemGroup>
4448
<PackageReference Include="BouncyCastle.Cryptography" />
4549
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" />
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
using System;
2+
using System.Security.Cryptography;
3+
4+
namespace Renci.SshNet.Security
5+
{
6+
internal sealed partial class KeyExchangeMLKem768X25519Sha256
7+
{
8+
private sealed class MLKemBclImpl : Impl
9+
{
10+
private MLKem _mlkem;
11+
12+
public override byte[] GenerateClientPublicKey()
13+
{
14+
_mlkem = MLKem.GenerateKey(MLKemAlgorithm.MLKem768);
15+
return _mlkem.ExportEncapsulationKey();
16+
}
17+
18+
public override byte[] CalculateAgreement(byte[] serverPublicKey)
19+
{
20+
var mlkemSecret = new byte[MLKemAlgorithm.MLKem768.SharedSecretSizeInBytes];
21+
_mlkem.Decapsulate(serverPublicKey.AsSpan(0, MLKemAlgorithm.MLKem768.CiphertextSizeInBytes), mlkemSecret);
22+
return mlkemSecret;
23+
}
24+
25+
protected override void Dispose(bool disposing)
26+
{
27+
if (disposing)
28+
{
29+
_mlkem?.Dispose();
30+
}
31+
32+
base.Dispose(disposing);
33+
}
34+
}
35+
}
36+
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
using Org.BouncyCastle.Crypto.Generators;
2+
using Org.BouncyCastle.Crypto.Kems;
3+
using Org.BouncyCastle.Crypto.Parameters;
4+
5+
using Renci.SshNet.Abstractions;
6+
7+
namespace Renci.SshNet.Security
8+
{
9+
internal sealed partial class KeyExchangeMLKem768X25519Sha256
10+
{
11+
private sealed class MLKemBouncyCastleImpl : Impl
12+
{
13+
private MLKemDecapsulator _mlkemDecapsulator;
14+
15+
public override byte[] GenerateClientPublicKey()
16+
{
17+
var mlkem768KeyPairGenerator = new MLKemKeyPairGenerator();
18+
mlkem768KeyPairGenerator.Init(new MLKemKeyGenerationParameters(CryptoAbstraction.SecureRandom, MLKemParameters.ml_kem_768));
19+
var mlkem768KeyPair = mlkem768KeyPairGenerator.GenerateKeyPair();
20+
21+
_mlkemDecapsulator = new MLKemDecapsulator(MLKemParameters.ml_kem_768);
22+
_mlkemDecapsulator.Init(mlkem768KeyPair.Private);
23+
24+
return ((MLKemPublicKeyParameters)mlkem768KeyPair.Public).GetEncoded();
25+
}
26+
27+
public override byte[] CalculateAgreement(byte[] serverPublicKey)
28+
{
29+
var mlkemSecret = new byte[_mlkemDecapsulator.SecretLength];
30+
_mlkemDecapsulator.Decapsulate(serverPublicKey, 0, _mlkemDecapsulator.EncapsulationLength, mlkemSecret, 0, _mlkemDecapsulator.SecretLength);
31+
32+
return mlkemSecret;
33+
}
34+
}
35+
}
36+
}

src/Renci.SshNet/Security/KeyExchangeMLKem768X25519Sha256.cs

Lines changed: 37 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
11
using System.Globalization;
2-
using System.Linq;
2+
using System.Security.Cryptography;
33

4-
using Org.BouncyCastle.Crypto.Generators;
5-
using Org.BouncyCastle.Crypto.Kems;
64
using Org.BouncyCastle.Crypto.Parameters;
75

86
using Renci.SshNet.Abstractions;
@@ -11,9 +9,15 @@
119

1210
namespace Renci.SshNet.Security
1311
{
14-
internal sealed class KeyExchangeMLKem768X25519Sha256 : KeyExchangeECCurve25519
12+
internal sealed partial class KeyExchangeMLKem768X25519Sha256 : KeyExchangeECCurve25519
1513
{
16-
private MLKemDecapsulator _mlkemDecapsulator;
14+
#if Test_BCL_MLKem
15+
private MLKemBclImpl _mlkemImpl;
16+
#elif Test_BouncyCastle_MLKem
17+
private MLKemBouncyCastleImpl _mlkemImpl;
18+
#else
19+
private Impl _mlkemImpl;
20+
#endif
1721

1822
/// <summary>
1923
/// Gets algorithm name.
@@ -41,14 +45,21 @@ protected override void StartImpl()
4145

4246
Session.KeyExchangeHybridReplyMessageReceived += Session_KeyExchangeHybridReplyMessageReceived;
4347

44-
var mlkem768KeyPairGenerator = new MLKemKeyPairGenerator();
45-
mlkem768KeyPairGenerator.Init(new MLKemKeyGenerationParameters(CryptoAbstraction.SecureRandom, MLKemParameters.ml_kem_768));
46-
var mlkem768KeyPair = mlkem768KeyPairGenerator.GenerateKeyPair();
47-
48-
_mlkemDecapsulator = new MLKemDecapsulator(MLKemParameters.ml_kem_768);
49-
_mlkemDecapsulator.Init(mlkem768KeyPair.Private);
50-
51-
var mlkem768PublicKey = ((MLKemPublicKeyParameters)mlkem768KeyPair.Public).GetEncoded();
48+
#if Test_BCL_MLKem
49+
_mlkemImpl = new MLKemBclImpl();
50+
#elif Test_BouncyCastle_MLKem
51+
_mlkemImpl = new MLKemBouncyCastleImpl();
52+
#else
53+
if (MLKem.IsSupported)
54+
{
55+
_mlkemImpl = new MLKemBclImpl();
56+
}
57+
else
58+
{
59+
_mlkemImpl = new MLKemBouncyCastleImpl();
60+
}
61+
#endif
62+
var mlkem768PublicKey = _mlkemImpl.GenerateClientPublicKey();
5263

5364
var x25519PublicKey = _impl.GenerateClientPublicKey();
5465

@@ -100,20 +111,28 @@ private void HandleServerHybridReply(byte[] hostKey, byte[] serverExchangeValue,
100111
_hostKey = hostKey;
101112
_signature = signature;
102113

103-
if (serverExchangeValue.Length != _mlkemDecapsulator.EncapsulationLength + X25519PublicKeyParameters.KeySize)
114+
if (serverExchangeValue.Length != MLKemAlgorithm.MLKem768.CiphertextSizeInBytes + X25519PublicKeyParameters.KeySize)
104115
{
105116
throw new SshConnectionException(
106117
string.Format(CultureInfo.CurrentCulture, "Bad S_Reply length: {0}.", serverExchangeValue.Length),
107118
DisconnectReason.KeyExchangeFailed);
108119
}
109120

110-
var mlkemSecret = new byte[_mlkemDecapsulator.SecretLength];
111-
112-
_mlkemDecapsulator.Decapsulate(serverExchangeValue, 0, _mlkemDecapsulator.EncapsulationLength, mlkemSecret, 0, _mlkemDecapsulator.SecretLength);
121+
var mlkemSecret = _mlkemImpl.CalculateAgreement(serverExchangeValue);
113122

114-
var x25519Agreement = _impl.CalculateAgreement(serverExchangeValue.Take(_mlkemDecapsulator.EncapsulationLength, X25519PublicKeyParameters.KeySize));
123+
var x25519Agreement = _impl.CalculateAgreement(serverExchangeValue.Take(MLKemAlgorithm.MLKem768.CiphertextSizeInBytes, X25519PublicKeyParameters.KeySize));
115124

116125
SharedKey = CryptoAbstraction.HashSHA256(mlkemSecret.Concat(x25519Agreement));
117126
}
127+
128+
protected override void Dispose(bool disposing)
129+
{
130+
if (disposing)
131+
{
132+
_mlkemImpl?.Dispose();
133+
}
134+
135+
base.Dispose(disposing);
136+
}
118137
}
119138
}

0 commit comments

Comments
 (0)