diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index bbf46a9f4..b03762d39 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -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: @@ -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: @@ -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 diff --git a/src/Renci.SshNet/Renci.SshNet.csproj b/src/Renci.SshNet/Renci.SshNet.csproj index 9b7af1590..2d906c1c9 100644 --- a/src/Renci.SshNet/Renci.SshNet.csproj +++ b/src/Renci.SshNet/Renci.SshNet.csproj @@ -49,14 +49,10 @@ - + - - - - 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 87614f8e6..eaeaeac0a 100644 --- a/src/Renci.SshNet/Security/KeyExchangeMLKem768X25519Sha256.cs +++ b/src/Renci.SshNet/Security/KeyExchangeMLKem768X25519Sha256.cs @@ -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; /// /// Gets algorithm name. @@ -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(); @@ -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); + } } }