Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
102 changes: 96 additions & 6 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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/
Expand All @@ -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 \
Expand All @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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
Expand Down
1 change: 1 addition & 0 deletions Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
<PackageVersion Include="GitHubActionsTestLogger" Version="2.4.1" />
<PackageVersion Include="Meziantou.Analyzer" Version="2.0.220" />
<!-- Should stay on LTS .NET releases. -->
<PackageVersion Include="Microsoft.Bcl.Cryptography" Version="10.0.0-rc.1.25451.107" />
<PackageVersion Include="Microsoft.Extensions.Logging.Abstractions" Version="8.0.3" />
<PackageVersion Include="Microsoft.Extensions.Logging.Console" Version="9.0.9" />
<PackageVersion Include="MSTest" Version="3.9.3" />
Expand Down
9 changes: 7 additions & 2 deletions src/Renci.SshNet/Renci.SshNet.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,10 @@
<IsAotCompatible>true</IsAotCompatible>
</PropertyGroup>

<PropertyGroup Condition="'$(Configuration)|$(TargetFramework)|$(Platform)'=='Debug|net462|AnyCPU'">
<DefineConstants>$(DefineConstants);Test_BCL_MLKem</DefineConstants>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="BouncyCastle.Cryptography" />
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" />
Expand All @@ -49,10 +53,11 @@
</PackageReference>
</ItemGroup>

<ItemGroup Condition=" !$([MSBuild]::IsTargetFrameworkCompatible('$(TargetFramework)', 'net8.0')) ">
<PackageReference Include="System.Formats.Asn1" />
<ItemGroup>
<PackageReference Include="Microsoft.Bcl.Cryptography" />
</ItemGroup>


<ItemGroup>
<None Include="..\..\images\logo\png\SS-NET-icon-h500.png">
<Pack>True</Pack>
Expand Down
Original file line number Diff line number Diff line change
@@ -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);
}
}
}
}
Original file line number Diff line number Diff line change
@@ -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;
}
}
}
}
55 changes: 37 additions & 18 deletions src/Renci.SshNet/Security/KeyExchangeMLKem768X25519Sha256.cs
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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

/// <summary>
/// Gets algorithm name.
Expand Down Expand Up @@ -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();

Expand Down Expand Up @@ -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);
}
}
}
Loading