diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index 9b3759d43..5f086f627 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -16,6 +16,9 @@ jobs:
- name: Setup .NET
uses: actions/setup-dotnet@v5
+ with:
+ dotnet-version: '10.0.x'
+ dotnet-quality: 'preview'
- name: Build Unit Tests .NET
run: dotnet build -f net9.0 test/Renci.SshNet.Tests/
@@ -35,7 +38,7 @@ jobs:
-p:CoverletOutput=../../coverlet/linux_unit_test_net_9_coverage.xml \
test/Renci.SshNet.Tests/
- - name: Run Integration Tests .NET
+ - name: Run Integration Tests .NET 1
run: |
dotnet test \
-f net9.0 \
@@ -44,7 +47,33 @@ jobs:
--logger GitHubActions \
-p:CollectCoverage=true \
-p:CoverletOutputFormat=cobertura \
- -p:CoverletOutput=../../coverlet/linux_integration_test_net_9_coverage.xml \
+ -p:CoverletOutput=../../coverlet/linux_integration_test_net_9_coverage_1.xml \
+ test/Renci.SshNet.IntegrationTests/
+
+ - name: Run Integration Tests .NET 2
+ run: |
+ dotnet test \
+ -f net9.0 \
+ --logger "console;verbosity=normal" \
+ --logger GitHubActions \
+ --filter "Name=MLKem768X25519Sha256" \
+ -p:DefineConstants="$(9);Test_BCL_MLKem" \
+ -p:CollectCoverage=true \
+ -p:CoverletOutputFormat=cobertura \
+ -p:CoverletOutput=../../coverlet/linux_integration_test_net_9_coverage_2.xml \
+ test/Renci.SshNet.IntegrationTests/
+
+ - name: Run Integration Tests .NET 3
+ run: |
+ dotnet test \
+ -f net9.0 \
+ --logger "console;verbosity=normal" \
+ --logger GitHubActions \
+ --filter "Name=MLKem768X25519Sha256" \
+ -p:DefineConstants="Test_BouncyCastle_MLKem" \
+ -p:CollectCoverage=true \
+ -p:CoverletOutputFormat=cobertura \
+ -p:CoverletOutput=../../coverlet/linux_integration_test_net_9_coverage_3.xml \
test/Renci.SshNet.IntegrationTests/
- name: Archive Coverlet Results
@@ -63,6 +92,9 @@ jobs:
- name: Setup .NET
uses: actions/setup-dotnet@v5
+ with:
+ dotnet-version: '10.0.x'
+ dotnet-quality: 'preview'
- name: Build Solution
run: dotnet build Renci.SshNet.sln
@@ -114,6 +146,9 @@ jobs:
- name: Setup .NET
uses: actions/setup-dotnet@v5
+ with:
+ dotnet-version: '10.0.x'
+ dotnet-quality: 'preview'
- name: Setup WSL2
uses: Vampire/setup-wsl@6a8db447be7ed35f2f499c02c6e60ff77ef11278 # v6.0.0
@@ -128,15 +163,41 @@ jobs:
podman build -t renci-ssh-tests-server-image -f test/Renci.SshNet.IntegrationTests/Dockerfile test/Renci.SshNet.IntegrationTests/
podman run --rm -h renci-ssh-tests-server -d -p 2222:22 renci-ssh-tests-server-image
- - name: Run Integration Tests .NET Framework
+ - name: Run Integration Tests .NET Framework 1
+ run:
+ dotnet test `
+ -f net48 `
+ --logger "console;verbosity=normal" `
+ --logger GitHubActions `
+ -p:CollectCoverage=true `
+ -p:CoverletOutputFormat=cobertura `
+ -p:CoverletOutput=..\..\coverlet\windows_integration_test_net_4_8_coverage_1.xml `
+ test\Renci.SshNet.IntegrationTests\
+
+ - name: Run Integration Tests .NET Framework 2
run:
dotnet test `
-f net48 `
--logger "console;verbosity=normal" `
--logger GitHubActions `
+ --filter "Name=MLKem768X25519Sha256" `
+ -p:DefineConstants="Test_BCL_MLKem" `
-p:CollectCoverage=true `
-p:CoverletOutputFormat=cobertura `
- -p:CoverletOutput=..\..\coverlet\windows_integration_test_net_4_8_coverage.xml `
+ -p:CoverletOutput=..\..\coverlet\windows_integration_test_net_4_8_coverage_2.xml `
+ test\Renci.SshNet.IntegrationTests\
+
+ - name: Run Integration Tests .NET Framework 3
+ run:
+ dotnet test `
+ -f net48 `
+ --logger "console;verbosity=normal" `
+ --logger GitHubActions `
+ --filter "Name=MLKem768X25519Sha256" `
+ -p:DefineConstants="Test_BouncyCastle_MLKem" `
+ -p:CollectCoverage=true `
+ -p:CoverletOutputFormat=cobertura `
+ -p:CoverletOutput=..\..\coverlet\windows_integration_test_net_4_8_coverage_3.xml `
test\Renci.SshNet.IntegrationTests\
- name: Archive Coverlet Results
@@ -156,6 +217,9 @@ jobs:
- name: Setup .NET
uses: actions/setup-dotnet@v5
+ with:
+ dotnet-version: '10.0.x'
+ dotnet-quality: 'preview'
- name: Setup WSL2
uses: Vampire/setup-wsl@6a8db447be7ed35f2f499c02c6e60ff77ef11278 # v6.0.0
@@ -170,15 +234,41 @@ jobs:
podman build -t renci-ssh-tests-server-image -f test/Renci.SshNet.IntegrationTests/Dockerfile test/Renci.SshNet.IntegrationTests/
podman run --rm -h renci-ssh-tests-server -d -p 2222:22 renci-ssh-tests-server-image
- - name: Run Integration Tests .NET
+ - name: Run Integration Tests .NET 1
+ run:
+ dotnet test `
+ -f net9.0 `
+ --logger "console;verbosity=normal" `
+ --logger GitHubActions `
+ -p:CollectCoverage=true `
+ -p:CoverletOutputFormat=cobertura `
+ -p:CoverletOutput=..\..\coverlet\windows_integration_test_net_9_coverage_1.xml `
+ test\Renci.SshNet.IntegrationTests\
+
+ - name: Run Integration Tests .NET 2
+ run:
+ dotnet test `
+ -f net9.0 `
+ --logger "console;verbosity=normal" `
+ --logger GitHubActions `
+ --filter "Name=MLKem768X25519Sha256" `
+ -p:DefineConstants="Test_BCL_MLKem" `
+ -p:CollectCoverage=true `
+ -p:CoverletOutputFormat=cobertura `
+ -p:CoverletOutput=..\..\coverlet\windows_integration_test_net_9_coverage_2.xml `
+ test\Renci.SshNet.IntegrationTests\
+
+ - name: Run Integration Tests .NET 3
run:
dotnet test `
-f net9.0 `
--logger "console;verbosity=normal" `
--logger GitHubActions `
+ --filter "Name=MLKem768X25519Sha256" `
+ -p:DefineConstants="Test_BouncyCastle_MLKem" `
-p:CollectCoverage=true `
-p:CoverletOutputFormat=cobertura `
- -p:CoverletOutput=..\..\coverlet\windows_integration_test_net_9_coverage.xml `
+ -p:CoverletOutput=..\..\coverlet\windows_integration_test_net_9_coverage_3.xml `
test\Renci.SshNet.IntegrationTests\
- name: Archive Coverlet Results
diff --git a/Directory.Packages.props b/Directory.Packages.props
index 8138199d5..1f884229e 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -11,6 +11,7 @@
+
diff --git a/src/Renci.SshNet/Renci.SshNet.csproj b/src/Renci.SshNet/Renci.SshNet.csproj
index 446865229..b7fb361ee 100644
--- a/src/Renci.SshNet/Renci.SshNet.csproj
+++ b/src/Renci.SshNet/Renci.SshNet.csproj
@@ -40,6 +40,10 @@
true
+
+ $(DefineConstants);Test_BCL_MLKem
+
+
@@ -49,10 +53,11 @@
-
-
+
+
+
True
diff --git a/src/Renci.SshNet/Security/KeyExchangeMLKem768X25519Sha256.BclImpl.cs b/src/Renci.SshNet/Security/KeyExchangeMLKem768X25519Sha256.BclImpl.cs
new file mode 100644
index 000000000..6cc22e522
--- /dev/null
+++ b/src/Renci.SshNet/Security/KeyExchangeMLKem768X25519Sha256.BclImpl.cs
@@ -0,0 +1,36 @@
+using System;
+using System.Security.Cryptography;
+
+namespace Renci.SshNet.Security
+{
+ internal sealed partial class KeyExchangeMLKem768X25519Sha256
+ {
+ private sealed class MLKemBclImpl : Impl
+ {
+ private MLKem _mlkem;
+
+ public override byte[] GenerateClientPublicKey()
+ {
+ _mlkem = MLKem.GenerateKey(MLKemAlgorithm.MLKem768);
+ return _mlkem.ExportEncapsulationKey();
+ }
+
+ public override byte[] CalculateAgreement(byte[] serverPublicKey)
+ {
+ var mlkemSecret = new byte[MLKemAlgorithm.MLKem768.SharedSecretSizeInBytes];
+ _mlkem.Decapsulate(serverPublicKey.AsSpan(0, MLKemAlgorithm.MLKem768.CiphertextSizeInBytes), mlkemSecret);
+ return mlkemSecret;
+ }
+
+ protected override void Dispose(bool disposing)
+ {
+ if (disposing)
+ {
+ _mlkem?.Dispose();
+ }
+
+ base.Dispose(disposing);
+ }
+ }
+ }
+}
diff --git a/src/Renci.SshNet/Security/KeyExchangeMLKem768X25519Sha256.BouncyCastleImpl.cs b/src/Renci.SshNet/Security/KeyExchangeMLKem768X25519Sha256.BouncyCastleImpl.cs
new file mode 100644
index 000000000..73eb3c08f
--- /dev/null
+++ b/src/Renci.SshNet/Security/KeyExchangeMLKem768X25519Sha256.BouncyCastleImpl.cs
@@ -0,0 +1,36 @@
+using Org.BouncyCastle.Crypto.Generators;
+using Org.BouncyCastle.Crypto.Kems;
+using Org.BouncyCastle.Crypto.Parameters;
+
+using Renci.SshNet.Abstractions;
+
+namespace Renci.SshNet.Security
+{
+ internal sealed partial class KeyExchangeMLKem768X25519Sha256
+ {
+ private sealed class MLKemBouncyCastleImpl : Impl
+ {
+ private MLKemDecapsulator _mlkemDecapsulator;
+
+ public override byte[] GenerateClientPublicKey()
+ {
+ var mlkem768KeyPairGenerator = new MLKemKeyPairGenerator();
+ mlkem768KeyPairGenerator.Init(new MLKemKeyGenerationParameters(CryptoAbstraction.SecureRandom, MLKemParameters.ml_kem_768));
+ var mlkem768KeyPair = mlkem768KeyPairGenerator.GenerateKeyPair();
+
+ _mlkemDecapsulator = new MLKemDecapsulator(MLKemParameters.ml_kem_768);
+ _mlkemDecapsulator.Init(mlkem768KeyPair.Private);
+
+ return ((MLKemPublicKeyParameters)mlkem768KeyPair.Public).GetEncoded();
+ }
+
+ public override byte[] CalculateAgreement(byte[] serverPublicKey)
+ {
+ var mlkemSecret = new byte[_mlkemDecapsulator.SecretLength];
+ _mlkemDecapsulator.Decapsulate(serverPublicKey, 0, _mlkemDecapsulator.EncapsulationLength, mlkemSecret, 0, _mlkemDecapsulator.SecretLength);
+
+ return mlkemSecret;
+ }
+ }
+ }
+}
diff --git a/src/Renci.SshNet/Security/KeyExchangeMLKem768X25519Sha256.cs b/src/Renci.SshNet/Security/KeyExchangeMLKem768X25519Sha256.cs
index 606e3a250..77cf83b7a 100644
--- a/src/Renci.SshNet/Security/KeyExchangeMLKem768X25519Sha256.cs
+++ b/src/Renci.SshNet/Security/KeyExchangeMLKem768X25519Sha256.cs
@@ -1,8 +1,6 @@
using System.Globalization;
-using System.Linq;
+using System.Security.Cryptography;
-using Org.BouncyCastle.Crypto.Generators;
-using Org.BouncyCastle.Crypto.Kems;
using Org.BouncyCastle.Crypto.Parameters;
using Renci.SshNet.Abstractions;
@@ -11,9 +9,15 @@
namespace Renci.SshNet.Security
{
- internal sealed class KeyExchangeMLKem768X25519Sha256 : KeyExchangeECCurve25519
+ internal sealed partial class KeyExchangeMLKem768X25519Sha256 : KeyExchangeECCurve25519
{
- private MLKemDecapsulator _mlkemDecapsulator;
+#if Test_BCL_MLKem
+ private MLKemBclImpl _mlkemImpl;
+#elif Test_BouncyCastle_MLKem
+ private MLKemBouncyCastleImpl _mlkemImpl;
+#else
+ private Impl _mlkemImpl;
+#endif
///
/// Gets algorithm name.
@@ -41,14 +45,21 @@ protected override void StartImpl()
Session.KeyExchangeHybridReplyMessageReceived += Session_KeyExchangeHybridReplyMessageReceived;
- var mlkem768KeyPairGenerator = new MLKemKeyPairGenerator();
- mlkem768KeyPairGenerator.Init(new MLKemKeyGenerationParameters(CryptoAbstraction.SecureRandom, MLKemParameters.ml_kem_768));
- var mlkem768KeyPair = mlkem768KeyPairGenerator.GenerateKeyPair();
-
- _mlkemDecapsulator = new MLKemDecapsulator(MLKemParameters.ml_kem_768);
- _mlkemDecapsulator.Init(mlkem768KeyPair.Private);
-
- var mlkem768PublicKey = ((MLKemPublicKeyParameters)mlkem768KeyPair.Public).GetEncoded();
+#if Test_BCL_MLKem
+ _mlkemImpl = new MLKemBclImpl();
+#elif Test_BouncyCastle_MLKem
+ _mlkemImpl = new MLKemBouncyCastleImpl();
+#else
+ if (MLKem.IsSupported)
+ {
+ _mlkemImpl = new MLKemBclImpl();
+ }
+ else
+ {
+ _mlkemImpl = new MLKemBouncyCastleImpl();
+ }
+#endif
+ var mlkem768PublicKey = _mlkemImpl.GenerateClientPublicKey();
var x25519PublicKey = _impl.GenerateClientPublicKey();
@@ -100,20 +111,28 @@ private void HandleServerHybridReply(byte[] hostKey, byte[] serverExchangeValue,
_hostKey = hostKey;
_signature = signature;
- if (serverExchangeValue.Length != _mlkemDecapsulator.EncapsulationLength + X25519PublicKeyParameters.KeySize)
+ if (serverExchangeValue.Length != MLKemAlgorithm.MLKem768.CiphertextSizeInBytes + X25519PublicKeyParameters.KeySize)
{
throw new SshConnectionException(
string.Format(CultureInfo.CurrentCulture, "Bad S_Reply length: {0}.", serverExchangeValue.Length),
DisconnectReason.KeyExchangeFailed);
}
- var mlkemSecret = new byte[_mlkemDecapsulator.SecretLength];
-
- _mlkemDecapsulator.Decapsulate(serverExchangeValue, 0, _mlkemDecapsulator.EncapsulationLength, mlkemSecret, 0, _mlkemDecapsulator.SecretLength);
+ var mlkemSecret = _mlkemImpl.CalculateAgreement(serverExchangeValue);
- var x25519Agreement = _impl.CalculateAgreement(serverExchangeValue.Take(_mlkemDecapsulator.EncapsulationLength, X25519PublicKeyParameters.KeySize));
+ var x25519Agreement = _impl.CalculateAgreement(serverExchangeValue.Take(MLKemAlgorithm.MLKem768.CiphertextSizeInBytes, X25519PublicKeyParameters.KeySize));
SharedKey = CryptoAbstraction.HashSHA256(mlkemSecret.Concat(x25519Agreement));
}
+
+ protected override void Dispose(bool disposing)
+ {
+ if (disposing)
+ {
+ _mlkemImpl?.Dispose();
+ }
+
+ base.Dispose(disposing);
+ }
}
}