Skip to content

Commit c9be3fb

Browse files
committed
Use BCL MLKem
1 parent e5ad82c commit c9be3fb

File tree

6 files changed

+210
-26
lines changed

6 files changed

+210
-26
lines changed

.github/workflows/build.yml

Lines changed: 93 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ jobs:
1616

1717
- name: Setup .NET
1818
uses: actions/setup-dotnet@v5
19+
with:
20+
dotnet-version: '10.0.x'
1921

2022
- name: Build Unit Tests .NET
2123
run: dotnet build -f net9.0 test/Renci.SshNet.Tests/
@@ -35,7 +37,7 @@ jobs:
3537
-p:CoverletOutput=../../coverlet/linux_unit_test_net_9_coverage.xml \
3638
test/Renci.SshNet.Tests/
3739
38-
- name: Run Integration Tests .NET
40+
- name: Run Integration Tests .NET 1
3941
run: |
4042
dotnet test \
4143
-f net9.0 \
@@ -44,7 +46,33 @@ jobs:
4446
--logger GitHubActions \
4547
-p:CollectCoverage=true \
4648
-p:CoverletOutputFormat=cobertura \
47-
-p:CoverletOutput=../../coverlet/linux_integration_test_net_9_coverage.xml \
49+
-p:CoverletOutput=../../coverlet/linux_integration_test_net_9_coverage_1.xml \
50+
test/Renci.SshNet.IntegrationTests/
51+
52+
- name: Run Integration Tests .NET 2
53+
run: |
54+
dotnet test \
55+
-f net9.0 \
56+
--logger "console;verbosity=normal" \
57+
--logger GitHubActions \
58+
--filter "Name=MLKem768X25519Sha256" \
59+
-p:DefineConstants="Test_BCL_MLKem" \
60+
-p:CollectCoverage=true \
61+
-p:CoverletOutputFormat=cobertura \
62+
-p:CoverletOutput=../../coverlet/linux_integration_test_net_9_coverage_2.xml \
63+
test/Renci.SshNet.IntegrationTests/
64+
65+
- name: Run Integration Tests .NET 3
66+
run: |
67+
dotnet test \
68+
-f net9.0 \
69+
--logger "console;verbosity=normal" \
70+
--logger GitHubActions \
71+
--filter "Name=MLKem768X25519Sha256" \
72+
-p:DefineConstants="Test_BouncyCastle_MLKem" \
73+
-p:CollectCoverage=true \
74+
-p:CoverletOutputFormat=cobertura \
75+
-p:CoverletOutput=../../coverlet/linux_integration_test_net_9_coverage_3.xml \
4876
test/Renci.SshNet.IntegrationTests/
4977
5078
- name: Archive Coverlet Results
@@ -63,6 +91,8 @@ jobs:
6391

6492
- name: Setup .NET
6593
uses: actions/setup-dotnet@v5
94+
with:
95+
dotnet-version: '10.0.x'
6696

6797
- name: Build Solution
6898
run: dotnet build Renci.SshNet.sln
@@ -114,6 +144,8 @@ jobs:
114144

115145
- name: Setup .NET
116146
uses: actions/setup-dotnet@v5
147+
with:
148+
dotnet-version: '10.0.x'
117149

118150
- name: Setup WSL2
119151
uses: Vampire/setup-wsl@6a8db447be7ed35f2f499c02c6e60ff77ef11278 # v6.0.0
@@ -128,15 +160,41 @@ jobs:
128160
podman build -t renci-ssh-tests-server-image -f test/Renci.SshNet.IntegrationTests/Dockerfile test/Renci.SshNet.IntegrationTests/
129161
podman run --rm -h renci-ssh-tests-server -d -p 2222:22 renci-ssh-tests-server-image
130162
131-
- name: Run Integration Tests .NET Framework
163+
- name: Run Integration Tests .NET Framework 1
164+
run:
165+
dotnet test `
166+
-f net48 `
167+
--logger "console;verbosity=normal" `
168+
--logger GitHubActions `
169+
-p:CollectCoverage=true `
170+
-p:CoverletOutputFormat=cobertura `
171+
-p:CoverletOutput=..\..\coverlet\windows_integration_test_net_4_8_coverage_1.xml `
172+
test\Renci.SshNet.IntegrationTests\
173+
174+
- name: Run Integration Tests .NET Framework 2
132175
run:
133176
dotnet test `
134177
-f net48 `
135178
--logger "console;verbosity=normal" `
136179
--logger GitHubActions `
180+
--filter "Name=MLKem768X25519Sha256" `
181+
-p:DefineConstants="Test_BCL_MLKem" `
137182
-p:CollectCoverage=true `
138183
-p:CoverletOutputFormat=cobertura `
139-
-p:CoverletOutput=..\..\coverlet\windows_integration_test_net_4_8_coverage.xml `
184+
-p:CoverletOutput=..\..\coverlet\windows_integration_test_net_4_8_coverage_2.xml `
185+
test\Renci.SshNet.IntegrationTests\
186+
187+
- name: Run Integration Tests .NET Framework 3
188+
run:
189+
dotnet test `
190+
-f net48 `
191+
--logger "console;verbosity=normal" `
192+
--logger GitHubActions `
193+
--filter "Name=MLKem768X25519Sha256" `
194+
-p:DefineConstants="Test_BouncyCastle_MLKem" `
195+
-p:CollectCoverage=true `
196+
-p:CoverletOutputFormat=cobertura `
197+
-p:CoverletOutput=..\..\coverlet\windows_integration_test_net_4_8_coverage_3.xml `
140198
test\Renci.SshNet.IntegrationTests\
141199

142200
- name: Archive Coverlet Results
@@ -156,6 +214,9 @@ jobs:
156214

157215
- name: Setup .NET
158216
uses: actions/setup-dotnet@v5
217+
with:
218+
dotnet-version: '10.0.x'
219+
dotnet-quality: 'preview'
159220

160221
- name: Setup WSL2
161222
uses: Vampire/setup-wsl@6a8db447be7ed35f2f499c02c6e60ff77ef11278 # v6.0.0
@@ -170,15 +231,41 @@ jobs:
170231
podman build -t renci-ssh-tests-server-image -f test/Renci.SshNet.IntegrationTests/Dockerfile test/Renci.SshNet.IntegrationTests/
171232
podman run --rm -h renci-ssh-tests-server -d -p 2222:22 renci-ssh-tests-server-image
172233
173-
- name: Run Integration Tests .NET
234+
- name: Run Integration Tests .NET 1
235+
run:
236+
dotnet test `
237+
-f net9.0 `
238+
--logger "console;verbosity=normal" `
239+
--logger GitHubActions `
240+
-p:CollectCoverage=true `
241+
-p:CoverletOutputFormat=cobertura `
242+
-p:CoverletOutput=..\..\coverlet\windows_integration_test_net_9_coverage_1.xml `
243+
test\Renci.SshNet.IntegrationTests\
244+
245+
- name: Run Integration Tests .NET 2
246+
run:
247+
dotnet test `
248+
-f net9.0 `
249+
--logger "console;verbosity=normal" `
250+
--logger GitHubActions `
251+
--filter "Name=MLKem768X25519Sha256" `
252+
-p:DefineConstants="Test_BCL_MLKem" `
253+
-p:CollectCoverage=true `
254+
-p:CoverletOutputFormat=cobertura `
255+
-p:CoverletOutput=..\..\coverlet\windows_integration_test_net_9_coverage_2.xml `
256+
test\Renci.SshNet.IntegrationTests\
257+
258+
- name: Run Integration Tests .NET 3
174259
run:
175260
dotnet test `
176261
-f net9.0 `
177262
--logger "console;verbosity=normal" `
178263
--logger GitHubActions `
264+
--filter "Name=MLKem768X25519Sha256" `
265+
-p:DefineConstants="Test_BouncyCastle_MLKem" `
179266
-p:CollectCoverage=true `
180267
-p:CoverletOutputFormat=cobertura `
181-
-p:CoverletOutput=..\..\coverlet\windows_integration_test_net_9_coverage.xml `
268+
-p:CoverletOutput=..\..\coverlet\windows_integration_test_net_9_coverage_3.xml `
182269
test\Renci.SshNet.IntegrationTests\
183270

184271
- name: Archive Coverlet Results

Directory.Packages.props

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
<PackageVersion Include="GitHubActionsTestLogger" Version="2.4.1" />
1212
<PackageVersion Include="Meziantou.Analyzer" Version="2.0.220" />
1313
<!-- Should stay on LTS .NET releases. -->
14+
<PackageVersion Include="Microsoft.Bcl.Cryptography" Version="10.0.0-rc.1.25451.107" />
1415
<PackageVersion Include="Microsoft.Extensions.Logging.Abstractions" Version="8.0.3" />
1516
<PackageVersion Include="Microsoft.Extensions.Logging.Console" Version="9.0.9" />
1617
<PackageVersion Include="MSTest" Version="3.9.3" />

src/Renci.SshNet/Renci.SshNet.csproj

Lines changed: 7 additions & 2 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" />
@@ -49,10 +53,11 @@
4953
</PackageReference>
5054
</ItemGroup>
5155

52-
<ItemGroup Condition=" !$([MSBuild]::IsTargetFrameworkCompatible('$(TargetFramework)', 'net8.0')) ">
53-
<PackageReference Include="System.Formats.Asn1" />
56+
<ItemGroup>
57+
<PackageReference Include="Microsoft.Bcl.Cryptography" />
5458
</ItemGroup>
5559

60+
5661
<ItemGroup>
5762
<None Include="..\..\images\logo\png\SS-NET-icon-h500.png">
5863
<Pack>True</Pack>
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)