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
12 changes: 11 additions & 1 deletion .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,11 @@ jobs:
name: Windows Integration Tests .NET Framework
runs-on: windows-2025
steps:
- name: Print Windows Version
shell: pwsh
run: |
Get-ItemProperty -Path "HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion"

- name: Checkout
uses: actions/checkout@v5
with:
Expand Down Expand Up @@ -149,6 +154,11 @@ jobs:
name: Windows Integration Tests .NET
runs-on: windows-2025
steps:
- name: Print Windows Version
shell: pwsh
run: |
Get-ItemProperty -Path "HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion"

- name: Checkout
uses: actions/checkout@v5
with:
Expand Down Expand Up @@ -178,7 +188,7 @@ jobs:
--logger GitHubActions `
-p:CollectCoverage=true `
-p:CoverletOutputFormat=cobertura `
-p:CoverletOutput=..\..\coverlet\windows_integration_test_net_9_coverage.xml `
-p:CoverletOutput=..\..\coverlet\windows_integration_test_net_10_coverage.xml `
test\Renci.SshNet.IntegrationTests\

- name: Archive Coverlet Results
Expand Down
6 changes: 1 addition & 5 deletions src/Renci.SshNet/Renci.SshNet.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -49,14 +49,10 @@
</PackageReference>
</ItemGroup>

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

<ItemGroup Condition=" '$(TargetFramework)' == 'netstandard2.0' ">
<PackageReference Include="System.Formats.Asn1" />
</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;
}
}
}
}
41 changes: 24 additions & 17 deletions src/Renci.SshNet/Security/KeyExchangeMLKem768X25519Sha256.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,16 @@
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;
using Renci.SshNet.Common;
using Renci.SshNet.Messages.Transport;

namespace Renci.SshNet.Security
{
internal sealed class KeyExchangeMLKem768X25519Sha256 : KeyExchangeECCurve25519
internal sealed partial class KeyExchangeMLKem768X25519Sha256 : KeyExchangeECCurve25519
{
private MLKemDecapsulator _mlkemDecapsulator;
private Impl _mlkemImpl;

/// <summary>
/// Gets algorithm name.
Expand Down Expand Up @@ -42,14 +39,16 @@ 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);
if (MLKem.IsSupported)
{
_mlkemImpl = new MLKemBclImpl();
}
else
{
_mlkemImpl = new MLKemBouncyCastleImpl();
}

var mlkem768PublicKey = ((MLKemPublicKeyParameters)mlkem768KeyPair.Public).GetEncoded();
var mlkem768PublicKey = _mlkemImpl.GenerateClientPublicKey();

var x25519PublicKey = _impl.GenerateClientPublicKey();

Expand Down Expand Up @@ -101,20 +100,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 = SHA256.HashData(mlkemSecret.Concat(x25519Agreement));
}

protected override void Dispose(bool disposing)
{
if (disposing)
{
_mlkemImpl?.Dispose();
}

base.Dispose(disposing);
}
}
}