Skip to content

Commit 4a83c5b

Browse files
committed
Use BCL MLKem
1 parent 4e02502 commit 4a83c5b

File tree

5 files changed

+193
-28
lines changed

5 files changed

+193
-28
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_10_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 net10.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_10_coverage.xml \
47+
-p:CoverletOutput=../../coverlet/linux_integration_test_net_10_coverage_1.xml \
48+
test/Renci.SshNet.IntegrationTests/
49+
50+
- name: Run Integration Tests .NET 2
51+
run: |
52+
dotnet test \
53+
-f net10.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_10_coverage_2.xml \
61+
test/Renci.SshNet.IntegrationTests/
62+
63+
- name: Run Integration Tests .NET 3
64+
run: |
65+
dotnet test \
66+
-f net10.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_10_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 net10.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_10_coverage_1.xml `
234+
test\Renci.SshNet.IntegrationTests\
235+
236+
- name: Run Integration Tests .NET 2
237+
run:
238+
dotnet test `
239+
-f net10.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_10_coverage_2.xml `
247+
test\Renci.SshNet.IntegrationTests\
248+
249+
- name: Run Integration Tests .NET 3
174250
run:
175251
dotnet test `
176252
-f net10.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_10_coverage_3.xml `
182260
test\Renci.SshNet.IntegrationTests\
183261

184262
- name: Archive Coverlet Results

src/Renci.SshNet/Renci.SshNet.csproj

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -49,14 +49,10 @@
4949
</PackageReference>
5050
</ItemGroup>
5151

52-
<ItemGroup Condition=" '$(TargetFramework)' == 'net462' ">
52+
<ItemGroup Condition=" !$([MSBuild]::IsTargetFrameworkCompatible('$(TargetFramework)', 'net10.0')) ">
5353
<PackageReference Include="Microsoft.Bcl.Cryptography" />
5454
</ItemGroup>
5555

56-
<ItemGroup Condition=" '$(TargetFramework)' == 'netstandard2.0' ">
57-
<PackageReference Include="System.Formats.Asn1" />
58-
</ItemGroup>
59-
6056
<ItemGroup>
6157
<None Include="..\..\images\logo\png\SS-NET-icon-h500.png">
6258
<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: 36 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,6 @@
22
using System.Linq;
33
using System.Security.Cryptography;
44

5-
using Org.BouncyCastle.Crypto.Generators;
6-
using Org.BouncyCastle.Crypto.Kems;
75
using Org.BouncyCastle.Crypto.Parameters;
86

97
using Renci.SshNet.Abstractions;
@@ -12,9 +10,15 @@
1210

1311
namespace Renci.SshNet.Security
1412
{
15-
internal sealed class KeyExchangeMLKem768X25519Sha256 : KeyExchangeECCurve25519
13+
internal sealed partial class KeyExchangeMLKem768X25519Sha256 : KeyExchangeECCurve25519
1614
{
17-
private MLKemDecapsulator _mlkemDecapsulator;
15+
#if Test_BCL_MLKem
16+
private MLKemBclImpl _mlkemImpl;
17+
#elif Test_BouncyCastle_MLKem
18+
private MLKemBouncyCastleImpl _mlkemImpl;
19+
#else
20+
private Impl _mlkemImpl;
21+
#endif
1822

1923
/// <summary>
2024
/// Gets algorithm name.
@@ -42,14 +46,21 @@ protected override void StartImpl()
4246

4347
Session.KeyExchangeHybridReplyMessageReceived += Session_KeyExchangeHybridReplyMessageReceived;
4448

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

5465
var x25519PublicKey = _impl.GenerateClientPublicKey();
5566

@@ -101,20 +112,28 @@ private void HandleServerHybridReply(byte[] hostKey, byte[] serverExchangeValue,
101112
_hostKey = hostKey;
102113
_signature = signature;
103114

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

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

115-
var x25519Agreement = _impl.CalculateAgreement(serverExchangeValue.Take(_mlkemDecapsulator.EncapsulationLength, X25519PublicKeyParameters.KeySize));
124+
var x25519Agreement = _impl.CalculateAgreement(serverExchangeValue.Take(MLKemAlgorithm.MLKem768.CiphertextSizeInBytes, X25519PublicKeyParameters.KeySize));
116125

117126
SharedKey = SHA256.HashData(mlkemSecret.Concat(x25519Agreement));
118127
}
128+
129+
protected override void Dispose(bool disposing)
130+
{
131+
if (disposing)
132+
{
133+
_mlkemImpl?.Dispose();
134+
}
135+
136+
base.Dispose(disposing);
137+
}
119138
}
120139
}

0 commit comments

Comments
 (0)