Skip to content

Commit baba8b9

Browse files
committed
Use BCL MLKem
1 parent e5ad82c commit baba8b9

File tree

6 files changed

+213
-26
lines changed

6 files changed

+213
-26
lines changed

.github/workflows/build.yml

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

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

2023
- name: Build Unit Tests .NET
2124
run: dotnet build -f net9.0 test/Renci.SshNet.Tests/
@@ -35,7 +38,7 @@ jobs:
3538
-p:CoverletOutput=../../coverlet/linux_unit_test_net_9_coverage.xml \
3639
test/Renci.SshNet.Tests/
3740
38-
- name: Run Integration Tests .NET
41+
- name: Run Integration Tests .NET 1
3942
run: |
4043
dotnet test \
4144
-f net9.0 \
@@ -44,7 +47,33 @@ jobs:
4447
--logger GitHubActions \
4548
-p:CollectCoverage=true \
4649
-p:CoverletOutputFormat=cobertura \
47-
-p:CoverletOutput=../../coverlet/linux_integration_test_net_9_coverage.xml \
50+
-p:CoverletOutput=../../coverlet/linux_integration_test_net_9_coverage_1.xml \
51+
test/Renci.SshNet.IntegrationTests/
52+
53+
- name: Run Integration Tests .NET 2
54+
run: |
55+
dotnet test \
56+
-f net9.0 \
57+
--logger "console;verbosity=normal" \
58+
--logger GitHubActions \
59+
--filter "Name=MLKem768X25519Sha256" \
60+
-p:DefineConstants="Test_BCL_MLKem" \
61+
-p:CollectCoverage=true \
62+
-p:CoverletOutputFormat=cobertura \
63+
-p:CoverletOutput=../../coverlet/linux_integration_test_net_9_coverage_2.xml \
64+
test/Renci.SshNet.IntegrationTests/
65+
66+
- name: Run Integration Tests .NET 3
67+
run: |
68+
dotnet test \
69+
-f net9.0 \
70+
--logger "console;verbosity=normal" \
71+
--logger GitHubActions \
72+
--filter "Name=MLKem768X25519Sha256" \
73+
-p:DefineConstants="Test_BouncyCastle_MLKem" \
74+
-p:CollectCoverage=true \
75+
-p:CoverletOutputFormat=cobertura \
76+
-p:CoverletOutput=../../coverlet/linux_integration_test_net_9_coverage_3.xml \
4877
test/Renci.SshNet.IntegrationTests/
4978
5079
- name: Archive Coverlet Results
@@ -63,6 +92,9 @@ jobs:
6392

6493
- name: Setup .NET
6594
uses: actions/setup-dotnet@v5
95+
with:
96+
dotnet-version: '10.0.x'
97+
dotnet-quality: 'preview'
6698

6799
- name: Build Solution
68100
run: dotnet build Renci.SshNet.sln
@@ -114,6 +146,9 @@ jobs:
114146

115147
- name: Setup .NET
116148
uses: actions/setup-dotnet@v5
149+
with:
150+
dotnet-version: '10.0.x'
151+
dotnet-quality: 'preview'
117152

118153
- name: Setup WSL2
119154
uses: Vampire/setup-wsl@6a8db447be7ed35f2f499c02c6e60ff77ef11278 # v6.0.0
@@ -128,15 +163,41 @@ jobs:
128163
podman build -t renci-ssh-tests-server-image -f test/Renci.SshNet.IntegrationTests/Dockerfile test/Renci.SshNet.IntegrationTests/
129164
podman run --rm -h renci-ssh-tests-server -d -p 2222:22 renci-ssh-tests-server-image
130165
131-
- name: Run Integration Tests .NET Framework
166+
- name: Run Integration Tests .NET Framework 1
167+
run:
168+
dotnet test `
169+
-f net48 `
170+
--logger "console;verbosity=normal" `
171+
--logger GitHubActions `
172+
-p:CollectCoverage=true `
173+
-p:CoverletOutputFormat=cobertura `
174+
-p:CoverletOutput=..\..\coverlet\windows_integration_test_net_4_8_coverage_1.xml `
175+
test\Renci.SshNet.IntegrationTests\
176+
177+
- name: Run Integration Tests .NET Framework 2
132178
run:
133179
dotnet test `
134180
-f net48 `
135181
--logger "console;verbosity=normal" `
136182
--logger GitHubActions `
183+
--filter "Name=MLKem768X25519Sha256" `
184+
-p:DefineConstants="Test_BCL_MLKem" `
137185
-p:CollectCoverage=true `
138186
-p:CoverletOutputFormat=cobertura `
139-
-p:CoverletOutput=..\..\coverlet\windows_integration_test_net_4_8_coverage.xml `
187+
-p:CoverletOutput=..\..\coverlet\windows_integration_test_net_4_8_coverage_2.xml `
188+
test\Renci.SshNet.IntegrationTests\
189+
190+
- name: Run Integration Tests .NET Framework 3
191+
run:
192+
dotnet test `
193+
-f net48 `
194+
--logger "console;verbosity=normal" `
195+
--logger GitHubActions `
196+
--filter "Name=MLKem768X25519Sha256" `
197+
-p:DefineConstants="Test_BouncyCastle_MLKem" `
198+
-p:CollectCoverage=true `
199+
-p:CoverletOutputFormat=cobertura `
200+
-p:CoverletOutput=..\..\coverlet\windows_integration_test_net_4_8_coverage_3.xml `
140201
test\Renci.SshNet.IntegrationTests\
141202

142203
- name: Archive Coverlet Results
@@ -156,6 +217,9 @@ jobs:
156217

157218
- name: Setup .NET
158219
uses: actions/setup-dotnet@v5
220+
with:
221+
dotnet-version: '10.0.x'
222+
dotnet-quality: 'preview'
159223

160224
- name: Setup WSL2
161225
uses: Vampire/setup-wsl@6a8db447be7ed35f2f499c02c6e60ff77ef11278 # v6.0.0
@@ -170,15 +234,41 @@ jobs:
170234
podman build -t renci-ssh-tests-server-image -f test/Renci.SshNet.IntegrationTests/Dockerfile test/Renci.SshNet.IntegrationTests/
171235
podman run --rm -h renci-ssh-tests-server -d -p 2222:22 renci-ssh-tests-server-image
172236
173-
- name: Run Integration Tests .NET
237+
- name: Run Integration Tests .NET 1
238+
run:
239+
dotnet test `
240+
-f net9.0 `
241+
--logger "console;verbosity=normal" `
242+
--logger GitHubActions `
243+
-p:CollectCoverage=true `
244+
-p:CoverletOutputFormat=cobertura `
245+
-p:CoverletOutput=..\..\coverlet\windows_integration_test_net_9_coverage_1.xml `
246+
test\Renci.SshNet.IntegrationTests\
247+
248+
- name: Run Integration Tests .NET 2
249+
run:
250+
dotnet test `
251+
-f net9.0 `
252+
--logger "console;verbosity=normal" `
253+
--logger GitHubActions `
254+
--filter "Name=MLKem768X25519Sha256" `
255+
-p:DefineConstants="Test_BCL_MLKem" `
256+
-p:CollectCoverage=true `
257+
-p:CoverletOutputFormat=cobertura `
258+
-p:CoverletOutput=..\..\coverlet\windows_integration_test_net_9_coverage_2.xml `
259+
test\Renci.SshNet.IntegrationTests\
260+
261+
- name: Run Integration Tests .NET 3
174262
run:
175263
dotnet test `
176264
-f net9.0 `
177265
--logger "console;verbosity=normal" `
178266
--logger GitHubActions `
267+
--filter "Name=MLKem768X25519Sha256" `
268+
-p:DefineConstants="Test_BouncyCastle_MLKem" `
179269
-p:CollectCoverage=true `
180270
-p:CoverletOutputFormat=cobertura `
181-
-p:CoverletOutput=..\..\coverlet\windows_integration_test_net_9_coverage.xml `
271+
-p:CoverletOutput=..\..\coverlet\windows_integration_test_net_9_coverage_3.xml `
182272
test\Renci.SshNet.IntegrationTests\
183273

184274
- 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)