Skip to content

Commit 60cde34

Browse files
committed
Use BCL MLKem
1 parent 8712c99 commit 60cde34

File tree

5 files changed

+193
-29
lines changed

5 files changed

+193
-29
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 & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -2,19 +2,22 @@
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

9-
using Renci.SshNet.Abstractions;
107
using Renci.SshNet.Common;
118
using Renci.SshNet.Messages.Transport;
129

1310
namespace Renci.SshNet.Security
1411
{
15-
internal sealed class KeyExchangeMLKem768X25519Sha256 : KeyExchangeECCurve25519
12+
internal sealed partial class KeyExchangeMLKem768X25519Sha256 : KeyExchangeECCurve25519
1613
{
17-
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
1821

1922
/// <summary>
2023
/// Gets algorithm name.
@@ -42,14 +45,21 @@ protected override void StartImpl()
4245

4346
Session.KeyExchangeHybridReplyMessageReceived += Session_KeyExchangeHybridReplyMessageReceived;
4447

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();
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();
5363

5464
var x25519PublicKey = _impl.GenerateClientPublicKey();
5565

@@ -101,20 +111,28 @@ private void HandleServerHybridReply(byte[] hostKey, byte[] serverExchangeValue,
101111
_hostKey = hostKey;
102112
_signature = signature;
103113

104-
if (serverExchangeValue.Length != _mlkemDecapsulator.EncapsulationLength + X25519PublicKeyParameters.KeySize)
114+
if (serverExchangeValue.Length != MLKemAlgorithm.MLKem768.CiphertextSizeInBytes + X25519PublicKeyParameters.KeySize)
105115
{
106116
throw new SshConnectionException(
107117
string.Format(CultureInfo.CurrentCulture, "Bad S_Reply length: {0}.", serverExchangeValue.Length),
108118
DisconnectReason.KeyExchangeFailed);
109119
}
110120

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

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

117125
SharedKey = SHA256.HashData(mlkemSecret.Concat(x25519Agreement));
118126
}
127+
128+
protected override void Dispose(bool disposing)
129+
{
130+
if (disposing)
131+
{
132+
_mlkemImpl?.Dispose();
133+
}
134+
135+
base.Dispose(disposing);
136+
}
119137
}
120138
}

0 commit comments

Comments
 (0)