From 752b1db8f0ff44df32b9b40664de8bd405c6e7bd Mon Sep 17 00:00:00 2001 From: mus65 Date: Sat, 23 Nov 2024 17:51:59 +0100 Subject: [PATCH 1/6] Add .NET 9 target (#1480) * Add .NET 9 target * Disable SonarSource S3236 This following change in the runtime now causes this analyzer to complain about some Debug.Assert calls which doesn't make sense. https://github.com/dotnet/core/blob/main/release-notes/9.0/preview/preview7/libraries.md#debugassert-now-reports-assert-condition-by-default https://rules.sonarsource.com/csharp/RSPEC-3236/ * make use of .NET 9 Lock type see https://github.com/dotnet/runtime/issues/34812 * Define own Lock type to avoid ifdefs * revert irrelevant style changes * update global.json * Keep net8.0 target in IntegrationTests Co-authored-by: Rob Hague * fix Package Downgrade Warning for some reason this happens starting with .NET 9.0 RC2: /home/mus/git/SSH.NET/test/Renci.SshNet.IntegrationTests/Renci.SshNet.IntegrationTests.csproj : error NU1605: Warning As Error: Detected package downgrade: BouncyCastle.Cryptography from 2.4.0 to 2.3.1. Reference the package directly from the project to select a different version. Renci.SshNet.IntegrationTests -> SSH.NET 1.0.0 -> BouncyCastle.Cryptography (>= 2.4.0) Renci.SshNet.IntegrationTests -> Testcontainers 3.10.0 -> BouncyCastle.Cryptography (>= 2.3.1) * update global.json to RC2 * update global.json to .NET 9 GA * update GitHub Actions for .NET 9 --------- Co-authored-by: Rob Hague --- .editorconfig | 3 +++ .github/workflows/build.yml | 18 +++++++++--------- global.json | 2 +- src/Renci.SshNet/Channels/Channel.cs | 4 ++-- .../Channels/ChannelDirectTcpip.cs | 2 +- .../Channels/ChannelForwardedTcpip.cs | 5 ++++- src/Renci.SshNet/Common/Lock.cs | 19 +++++++++++++++++++ src/Renci.SshNet/Renci.SshNet.csproj | 2 +- src/Renci.SshNet/Session.cs | 8 ++++---- src/Renci.SshNet/Sftp/SftpFileStream.cs | 2 +- src/Renci.SshNet/SshMessageFactory.cs | 5 ++++- .../Renci.SshNet.Benchmarks.csproj | 2 +- .../Renci.SshNet.IntegrationBenchmarks.csproj | 2 +- .../Renci.SshNet.IntegrationTests.csproj | 2 +- .../Common/AsyncSocketListener.cs | 6 ++++-- .../Renci.SshNet.Tests.csproj | 2 +- 16 files changed, 57 insertions(+), 27 deletions(-) create mode 100644 src/Renci.SshNet/Common/Lock.cs diff --git a/.editorconfig b/.editorconfig index 562309d42..c897d8a93 100644 --- a/.editorconfig +++ b/.editorconfig @@ -195,6 +195,9 @@ dotnet_diagnostic.S2971.severity = none # This is rather harmless. dotnet_diagnostic.S3218.severity = none +# S3236: Remove this argument from the method call; it hides the caller information. +dotnet_diagnostic.S3236.severity = none + # S3267: Loops should be simplified with "LINQ" expressions # https://rules.sonarsource.com/csharp/RSPEC-3267 # diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 6e4b721d2..458e018c5 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -17,13 +17,13 @@ jobs: - name: Setup .NET uses: actions/setup-dotnet@v4 with: - dotnet-version: 8.0.x + dotnet-version: 9.0.x - name: Build Unit Tests .NET - run: dotnet build -f net8.0 test/Renci.SshNet.Tests/ + run: dotnet build -f net9.0 test/Renci.SshNet.Tests/ - name: Build IntegrationTests .NET - run: dotnet build -f net8.0 test/Renci.SshNet.IntegrationTests/ + run: dotnet build -f net9.0 test/Renci.SshNet.IntegrationTests/ - name: Build IntegrationTests .NET Framework run: dotnet build -f net48 test/Renci.SshNet.IntegrationTests/ @@ -31,25 +31,25 @@ jobs: - name: Run Unit Tests .NET run: | dotnet test \ - -f net8.0 \ + -f net9.0 \ --no-build \ --logger "console;verbosity=normal" \ --logger GitHubActions \ -p:CollectCoverage=true \ -p:CoverletOutputFormat=cobertura \ - -p:CoverletOutput=../../coverlet/linux_unit_test_net_8_coverage.xml \ + -p:CoverletOutput=../../coverlet/linux_unit_test_net_9_coverage.xml \ test/Renci.SshNet.Tests/ - name: Run Integration Tests .NET run: | dotnet test \ - -f net8.0 \ + -f net9.0 \ --no-build \ --logger "console;verbosity=normal" \ --logger GitHubActions \ -p:CollectCoverage=true \ -p:CoverletOutputFormat=cobertura \ - -p:CoverletOutput=../../coverlet/linux_integration_test_net_8_coverage.xml \ + -p:CoverletOutput=../../coverlet/linux_integration_test_net_9_coverage.xml \ test/Renci.SshNet.IntegrationTests/ # Also run a subset of the integration tests targeting netfx using mono. This is a temporary measure to get @@ -111,13 +111,13 @@ jobs: - name: Run Unit Tests .NET run: | dotnet test ` - -f net8.0 ` + -f net9.0 ` --no-build ` --logger "console;verbosity=normal" ` --logger GitHubActions ` -p:CollectCoverage=true ` -p:CoverletOutputFormat=cobertura ` - -p:CoverletOutput=../../coverlet/windows_unit_test_net_8_coverage.xml ` + -p:CoverletOutput=../../coverlet/windows_unit_test_net_9_coverage.xml ` test/Renci.SshNet.Tests/ - name: Run Unit Tests .NET Framework diff --git a/global.json b/global.json index d07970ac2..f65788943 100644 --- a/global.json +++ b/global.json @@ -1,6 +1,6 @@ { "sdk": { - "version": "8.0.100", + "version": "9.0.100", "rollForward": "latestMajor" } } diff --git a/src/Renci.SshNet/Channels/Channel.cs b/src/Renci.SshNet/Channels/Channel.cs index 4c0909f26..ca8e910fa 100644 --- a/src/Renci.SshNet/Channels/Channel.cs +++ b/src/Renci.SshNet/Channels/Channel.cs @@ -14,8 +14,8 @@ namespace Renci.SshNet.Channels /// internal abstract class Channel : IChannel { - private readonly object _serverWindowSizeLock = new object(); - private readonly object _messagingLock = new object(); + private readonly Lock _serverWindowSizeLock = new Lock(); + private readonly Lock _messagingLock = new Lock(); private readonly uint _initialWindowSize; private readonly ISession _session; private EventWaitHandle _channelClosedWaitHandle = new ManualResetEvent(initialState: false); diff --git a/src/Renci.SshNet/Channels/ChannelDirectTcpip.cs b/src/Renci.SshNet/Channels/ChannelDirectTcpip.cs index 6a07d59b0..7b00c3bae 100644 --- a/src/Renci.SshNet/Channels/ChannelDirectTcpip.cs +++ b/src/Renci.SshNet/Channels/ChannelDirectTcpip.cs @@ -14,7 +14,7 @@ namespace Renci.SshNet.Channels /// internal sealed class ChannelDirectTcpip : ClientChannel, IChannelDirectTcpip { - private readonly object _socketLock = new object(); + private readonly Lock _socketLock = new Lock(); private EventWaitHandle _channelOpen = new AutoResetEvent(initialState: false); private EventWaitHandle _channelData = new AutoResetEvent(initialState: false); diff --git a/src/Renci.SshNet/Channels/ChannelForwardedTcpip.cs b/src/Renci.SshNet/Channels/ChannelForwardedTcpip.cs index 07271c834..2cc0c16f8 100644 --- a/src/Renci.SshNet/Channels/ChannelForwardedTcpip.cs +++ b/src/Renci.SshNet/Channels/ChannelForwardedTcpip.cs @@ -1,6 +1,9 @@ using System; using System.Net; using System.Net.Sockets; +#if NET9_0_OR_GREATER +using System.Threading; +#endif using Renci.SshNet.Abstractions; using Renci.SshNet.Common; @@ -13,7 +16,7 @@ namespace Renci.SshNet.Channels /// internal sealed class ChannelForwardedTcpip : ServerChannel, IChannelForwardedTcpip { - private readonly object _socketShutdownAndCloseLock = new object(); + private readonly Lock _socketShutdownAndCloseLock = new Lock(); private Socket _socket; private IForwardedPort _forwardedPort; diff --git a/src/Renci.SshNet/Common/Lock.cs b/src/Renci.SshNet/Common/Lock.cs new file mode 100644 index 000000000..fc29776d3 --- /dev/null +++ b/src/Renci.SshNet/Common/Lock.cs @@ -0,0 +1,19 @@ +#if !NET9_0_OR_GREATER +using System.Threading; + +namespace Renci.SshNet.Common +{ + internal sealed class Lock + { + public bool TryEnter() + { + return Monitor.TryEnter(this); + } + + public void Exit() + { + Monitor.Exit(this); + } + } +} +#endif diff --git a/src/Renci.SshNet/Renci.SshNet.csproj b/src/Renci.SshNet/Renci.SshNet.csproj index bf822da85..d00f773fa 100644 --- a/src/Renci.SshNet/Renci.SshNet.csproj +++ b/src/Renci.SshNet/Renci.SshNet.csproj @@ -4,7 +4,7 @@ Renci.SshNet SSH.NET SSH.NET - net462;netstandard2.0;netstandard2.1;net6.0;net7.0;net8.0 + net462;netstandard2.0;netstandard2.1;net6.0;net7.0;net8.0;net9.0 diff --git a/src/Renci.SshNet/Session.cs b/src/Renci.SshNet/Session.cs index d914858bb..63560b685 100644 --- a/src/Renci.SshNet/Session.cs +++ b/src/Renci.SshNet/Session.cs @@ -80,7 +80,7 @@ public class Session : ISession /// Holds an object that is used to ensure only a single thread can read from /// at any given time. /// - private readonly object _socketReadLock = new object(); + private readonly Lock _socketReadLock = new Lock(); /// /// Holds an object that is used to ensure only a single thread can write to @@ -90,7 +90,7 @@ public class Session : ISession /// This is also used to ensure that is /// incremented atomatically. /// - private readonly object _socketWriteLock = new object(); + private readonly Lock _socketWriteLock = new Lock(); /// /// Holds an object that is used to ensure only a single thread can dispose @@ -1894,7 +1894,7 @@ private bool IsSocketConnected() return false; } - if (!Monitor.TryEnter(_socketReadLock)) + if (!_socketReadLock.TryEnter()) { return true; } @@ -1906,7 +1906,7 @@ private bool IsSocketConnected() } finally { - Monitor.Exit(_socketReadLock); + _socketReadLock.Exit(); } } finally diff --git a/src/Renci.SshNet/Sftp/SftpFileStream.cs b/src/Renci.SshNet/Sftp/SftpFileStream.cs index 97b0f393a..8ef5d960d 100644 --- a/src/Renci.SshNet/Sftp/SftpFileStream.cs +++ b/src/Renci.SshNet/Sftp/SftpFileStream.cs @@ -18,7 +18,7 @@ namespace Renci.SshNet.Sftp #pragma warning restore IDE0079 public class SftpFileStream : Stream { - private readonly object _lock = new object(); + private readonly Lock _lock = new Lock(); private readonly int _readBufferSize; private readonly int _writeBufferSize; diff --git a/src/Renci.SshNet/SshMessageFactory.cs b/src/Renci.SshNet/SshMessageFactory.cs index c9b8e8c10..e023f5bb0 100644 --- a/src/Renci.SshNet/SshMessageFactory.cs +++ b/src/Renci.SshNet/SshMessageFactory.cs @@ -1,6 +1,9 @@ using System; using System.Collections.Generic; using System.Globalization; +#if NET9_0_OR_GREATER +using System.Threading; +#endif using Renci.SshNet.Common; using Renci.SshNet.Messages; @@ -14,7 +17,7 @@ internal sealed class SshMessageFactory { private readonly MessageMetadata[] _enabledMessagesByNumber; private readonly bool[] _activatedMessagesById; - private readonly object _lock = new object(); + private readonly Lock _lock = new Lock(); internal static readonly MessageMetadata[] AllMessages = new MessageMetadata[] { diff --git a/test/Renci.SshNet.Benchmarks/Renci.SshNet.Benchmarks.csproj b/test/Renci.SshNet.Benchmarks/Renci.SshNet.Benchmarks.csproj index d07465ef1..f421ad23f 100644 --- a/test/Renci.SshNet.Benchmarks/Renci.SshNet.Benchmarks.csproj +++ b/test/Renci.SshNet.Benchmarks/Renci.SshNet.Benchmarks.csproj @@ -2,7 +2,7 @@ Exe - net8.0 + net9.0 enable enable diff --git a/test/Renci.SshNet.IntegrationBenchmarks/Renci.SshNet.IntegrationBenchmarks.csproj b/test/Renci.SshNet.IntegrationBenchmarks/Renci.SshNet.IntegrationBenchmarks.csproj index 0f149c013..3f32fe9bb 100644 --- a/test/Renci.SshNet.IntegrationBenchmarks/Renci.SshNet.IntegrationBenchmarks.csproj +++ b/test/Renci.SshNet.IntegrationBenchmarks/Renci.SshNet.IntegrationBenchmarks.csproj @@ -2,7 +2,7 @@ Exe - net8.0 + net9.0 enable enable diff --git a/test/Renci.SshNet.IntegrationTests/Renci.SshNet.IntegrationTests.csproj b/test/Renci.SshNet.IntegrationTests/Renci.SshNet.IntegrationTests.csproj index 5fa390ad4..06ae83ea6 100644 --- a/test/Renci.SshNet.IntegrationTests/Renci.SshNet.IntegrationTests.csproj +++ b/test/Renci.SshNet.IntegrationTests/Renci.SshNet.IntegrationTests.csproj @@ -1,7 +1,7 @@  - net48;net8.0 + net48;net8.0;net9.0 enable true $(NoWarn);SYSLIB0021;SYSLIB1045;SYSLIB0014;IDE0220;IDE0010 diff --git a/test/Renci.SshNet.Tests/Common/AsyncSocketListener.cs b/test/Renci.SshNet.Tests/Common/AsyncSocketListener.cs index a27db194d..23e48799a 100644 --- a/test/Renci.SshNet.Tests/Common/AsyncSocketListener.cs +++ b/test/Renci.SshNet.Tests/Common/AsyncSocketListener.cs @@ -4,6 +4,8 @@ using System.Net; using System.Net.Sockets; using System.Threading; + +using Renci.SshNet.Common; #pragma warning restore IDE0005 namespace Renci.SshNet.Tests.Common @@ -13,7 +15,7 @@ public class AsyncSocketListener : IDisposable private readonly IPEndPoint _endPoint; private readonly ManualResetEvent _acceptCallbackDone; private readonly List _connectedClients; - private readonly object _syncLock; + private readonly Lock _syncLock; private Socket _listener; private Thread _receiveThread; private bool _started; @@ -31,7 +33,7 @@ public AsyncSocketListener(IPEndPoint endPoint) _endPoint = endPoint; _acceptCallbackDone = new ManualResetEvent(false); _connectedClients = new List(); - _syncLock = new object(); + _syncLock = new Lock(); ShutdownRemoteCommunicationSocket = true; } diff --git a/test/Renci.SshNet.Tests/Renci.SshNet.Tests.csproj b/test/Renci.SshNet.Tests/Renci.SshNet.Tests.csproj index df872d754..4b91384d3 100644 --- a/test/Renci.SshNet.Tests/Renci.SshNet.Tests.csproj +++ b/test/Renci.SshNet.Tests/Renci.SshNet.Tests.csproj @@ -1,7 +1,7 @@  - net462;net6.0;net7.0;net8.0 + net462;net6.0;net7.0;net8.0;net9.0 From f50fdcc24925bfc3a495a973050bcd8a10ea4e5b Mon Sep 17 00:00:00 2001 From: mus65 Date: Sat, 30 Nov 2024 19:12:11 +0100 Subject: [PATCH 2/6] Remove appveyor.yml (#1547) Follow-up #1539. Looks like AppVeyor has been disabled. --- appveyor.yml | 5 ----- 1 file changed, 5 deletions(-) delete mode 100644 appveyor.yml diff --git a/appveyor.yml b/appveyor.yml deleted file mode 100644 index 99d01900f..000000000 --- a/appveyor.yml +++ /dev/null @@ -1,5 +0,0 @@ -# empty to avoid AppVeyor errors until it's disabled -image: Ubuntu2204 - -build_script: - - echo done \ No newline at end of file From 70c12467dd88c3b958e35f677b0f4abe37090e2a Mon Sep 17 00:00:00 2001 From: mus65 Date: Mon, 2 Dec 2024 23:15:48 +0100 Subject: [PATCH 3/6] Replace DiagnosticAbstration with Microsoft.Extensions.Logging.Abstractions (#1509) * Replace DiagnosticAbstrations with Microsoft.Extensions.Logging.Abstractions * add documentation * reduce allocations by SessionId hex conversion generate the hex string once instead of every log call and optimize ToHex(). * Update docfx/logging.md Co-authored-by: Rob Hague * reduce log levels * hook up testcontainers logging * drop packet logs further down to trace * add kex traces --------- Co-authored-by: Rob Hague --- .editorconfig | 3 + CONTRIBUTING.md | 4 +- Directory.Packages.props | 3 + README.md | 1 + docfx/logging.md | 15 +++ .../Abstractions/DiagnosticAbstraction.cs | 69 ---------- src/Renci.SshNet/BaseClient.cs | 9 +- src/Renci.SshNet/Channels/Channel.cs | 7 +- .../Channels/ChannelDirectTcpip.cs | 8 +- .../Channels/ChannelForwardedTcpip.cs | 7 +- src/Renci.SshNet/Common/Extensions.cs | 7 + src/Renci.SshNet/Connection/ConnectorBase.cs | 9 +- src/Renci.SshNet/ForwardedPortDynamic.cs | 7 +- src/Renci.SshNet/ForwardedPortLocal.cs | 8 +- src/Renci.SshNet/ForwardedPortRemote.cs | 7 +- src/Renci.SshNet/Renci.SshNet.csproj | 1 + src/Renci.SshNet/Security/KeyExchange.cs | 120 +++++++++++++++--- src/Renci.SshNet/ServiceFactory.cs | 12 +- src/Renci.SshNet/Session.cs | 90 ++++++++----- src/Renci.SshNet/Sftp/SftpFileReader.cs | 6 +- .../SshNetLoggingConfiguration.cs | 26 ++++ src/Renci.SshNet/SubsystemSession.cs | 7 +- .../Renci.SshNet.IntegrationTests.csproj | 2 + .../TestsFixtures/InfrastructureFixture.cs | 24 ++-- .../TestsFixtures/IntegrationTestBase.cs | 29 +---- 25 files changed, 300 insertions(+), 181 deletions(-) create mode 100644 docfx/logging.md delete mode 100644 src/Renci.SshNet/Abstractions/DiagnosticAbstraction.cs create mode 100644 src/Renci.SshNet/SshNetLoggingConfiguration.cs diff --git a/.editorconfig b/.editorconfig index c897d8a93..aebbfc80f 100644 --- a/.editorconfig +++ b/.editorconfig @@ -704,6 +704,9 @@ dotnet_code_quality.CA1828.api_surface = all # Similar to MA0053, but does not support public types and types that define (new) virtual members. dotnet_diagnostic.CA1852.severity = none +# CA1848: don't enforce LoggerMessage pattern +dotnet_diagnostic.CA1848.severity = suggestion + # CA1859: Change return type for improved performance # # By default, this diagnostic is only reported for private members. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index ce029a36f..db38582f4 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -34,9 +34,9 @@ The repository makes use of continuous integration (CI) with GitHub Actions to v ## Good to know -### TraceSource logging +### Logging -The Debug build of SSH.NET contains rudimentary logging functionality via `System.Diagnostics.TraceSource`. See `Renci.SshNet.Abstractions.DiagnosticAbstraction` for usage examples. +The tests always log to the console. See the [Logging documentation](https://sshnet.github.io/SSH.NET/logging.html) on how to set a custom `ILoggerFactory`. ### Wireshark diff --git a/Directory.Packages.props b/Directory.Packages.props index 8aa6398e7..3964b45d7 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -15,6 +15,9 @@ + + + diff --git a/README.md b/README.md index 1daebcf5f..c308a6bfd 100644 --- a/README.md +++ b/README.md @@ -62,6 +62,7 @@ The main types provided by this library are: ## Additional Documentation * [Further examples](https://sshnet.github.io/SSH.NET/examples.html) +* [Logging](https://sshnet.github.io/SSH.NET/logging.html) * [API browser](https://sshnet.github.io/SSH.NET/api/Renci.SshNet.html) ## Encryption Methods diff --git a/docfx/logging.md b/docfx/logging.md new file mode 100644 index 000000000..9c0693412 --- /dev/null +++ b/docfx/logging.md @@ -0,0 +1,15 @@ +Logging +================= + +SSH.NET uses the [Microsoft.Extensions.Logging](https://learn.microsoft.com/dotnet/core/extensions/logging) API to log diagnostic messages. In order to access the log messages of SSH.NET in your own application for diagnosis, register your own `ILoggerFactory` before using the SSH.NET APIs, for example: + +```cs +ILoggerFactory loggerFactory = LoggerFactory.Create(builder => +{ + builder.SetMinimumLevel(LogLevel.Debug); + builder.AddConsole(); +}); + +Renci.SshNet.SshNetLoggingConfiguration.InitializeLogging(loggerFactory); + +All messages by SSH.NET are logged under the `Renci.SshNet` category. diff --git a/src/Renci.SshNet/Abstractions/DiagnosticAbstraction.cs b/src/Renci.SshNet/Abstractions/DiagnosticAbstraction.cs deleted file mode 100644 index bc1248dc0..000000000 --- a/src/Renci.SshNet/Abstractions/DiagnosticAbstraction.cs +++ /dev/null @@ -1,69 +0,0 @@ -using System.ComponentModel; -using System.Diagnostics; - -namespace Renci.SshNet.Abstractions -{ - /// - /// Provides access to the internals of SSH.NET. - /// - [EditorBrowsable(EditorBrowsableState.Never)] - public static class DiagnosticAbstraction - { - /// - /// The instance used by SSH.NET. - /// - /// - /// - /// Currently, the library only traces events when compiled in Debug mode. - /// - /// - /// Configuration on .NET Core must be done programmatically, e.g. - /// - /// DiagnosticAbstraction.Source.Switch = new SourceSwitch("sourceSwitch", "Verbose"); - /// DiagnosticAbstraction.Source.Listeners.Remove("Default"); - /// DiagnosticAbstraction.Source.Listeners.Add(new ConsoleTraceListener()); - /// DiagnosticAbstraction.Source.Listeners.Add(new TextWriterTraceListener("trace.log")); - /// - /// - /// - /// On .NET Framework, it is possible to configure via App.config, e.g. - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// ]]> - /// - /// - /// - public static readonly TraceSource Source = new TraceSource("SshNet.Logging"); - - /// - /// Logs a message to at the - /// level. - /// - /// The message to log. - /// The trace event type. - [Conditional("DEBUG")] - public static void Log(string text, TraceEventType type = TraceEventType.Verbose) - { - Source.TraceEvent(type, - System.Environment.CurrentManagedThreadId, - text); - } - } -} diff --git a/src/Renci.SshNet/BaseClient.cs b/src/Renci.SshNet/BaseClient.cs index 3757878b4..876710db9 100644 --- a/src/Renci.SshNet/BaseClient.cs +++ b/src/Renci.SshNet/BaseClient.cs @@ -4,7 +4,8 @@ using System.Threading; using System.Threading.Tasks; -using Renci.SshNet.Abstractions; +using Microsoft.Extensions.Logging; + using Renci.SshNet.Common; using Renci.SshNet.Messages.Transport; @@ -20,6 +21,7 @@ public abstract class BaseClient : IBaseClient /// private readonly bool _ownsConnectionInfo; + private readonly ILogger _logger; private readonly IServiceFactory _serviceFactory; private readonly object _keepAliveLock = new object(); private TimeSpan _keepAliveInterval; @@ -190,6 +192,7 @@ private protected BaseClient(ConnectionInfo connectionInfo, bool ownsConnectionI _connectionInfo = connectionInfo; _ownsConnectionInfo = ownsConnectionInfo; _serviceFactory = serviceFactory; + _logger = SshNetLoggingConfiguration.LoggerFactory.CreateLogger(GetType()); _keepAliveInterval = Timeout.InfiniteTimeSpan; } @@ -343,7 +346,7 @@ public async Task ConnectAsync(CancellationToken cancellationToken) /// The method was called after the client was disposed. public void Disconnect() { - DiagnosticAbstraction.Log("Disconnecting client."); + _logger.LogInformation("Disconnecting client."); CheckDisposed(); @@ -442,7 +445,7 @@ protected virtual void Dispose(bool disposing) if (disposing) { - DiagnosticAbstraction.Log("Disposing client."); + _logger.LogDebug("Disposing client."); Disconnect(); diff --git a/src/Renci.SshNet/Channels/Channel.cs b/src/Renci.SshNet/Channels/Channel.cs index ca8e910fa..4c3569cef 100644 --- a/src/Renci.SshNet/Channels/Channel.cs +++ b/src/Renci.SshNet/Channels/Channel.cs @@ -2,7 +2,8 @@ using System.Net.Sockets; using System.Threading; -using Renci.SshNet.Abstractions; +using Microsoft.Extensions.Logging; + using Renci.SshNet.Common; using Renci.SshNet.Messages; using Renci.SshNet.Messages.Connection; @@ -18,6 +19,7 @@ internal abstract class Channel : IChannel private readonly Lock _messagingLock = new Lock(); private readonly uint _initialWindowSize; private readonly ISession _session; + private readonly ILogger _logger; private EventWaitHandle _channelClosedWaitHandle = new ManualResetEvent(initialState: false); private EventWaitHandle _channelServerWindowAdjustWaitHandle = new ManualResetEvent(initialState: false); private uint? _remoteWindowSize; @@ -81,6 +83,7 @@ protected Channel(ISession session, uint localChannelNumber, uint localWindowSiz LocalChannelNumber = localChannelNumber; LocalPacketSize = localPacketSize; LocalWindowSize = localWindowSize; + _logger = SshNetLoggingConfiguration.LoggerFactory.CreateLogger(GetType()); session.ChannelWindowAdjustReceived += OnChannelWindowAdjust; session.ChannelDataReceived += OnChannelData; @@ -555,7 +558,7 @@ protected virtual void Close() var closeWaitResult = _session.TryWait(_channelClosedWaitHandle, ConnectionInfo.ChannelCloseTimeout); if (closeWaitResult != WaitResult.Success) { - DiagnosticAbstraction.Log(string.Format("Wait for channel close not successful: {0:G}.", closeWaitResult)); + _logger.LogInformation("Wait for channel close not successful: {CloseWaitResult}", closeWaitResult); } } } diff --git a/src/Renci.SshNet/Channels/ChannelDirectTcpip.cs b/src/Renci.SshNet/Channels/ChannelDirectTcpip.cs index 7b00c3bae..71108e309 100644 --- a/src/Renci.SshNet/Channels/ChannelDirectTcpip.cs +++ b/src/Renci.SshNet/Channels/ChannelDirectTcpip.cs @@ -3,6 +3,8 @@ using System.Net.Sockets; using System.Threading; +using Microsoft.Extensions.Logging; + using Renci.SshNet.Abstractions; using Renci.SshNet.Common; using Renci.SshNet.Messages.Connection; @@ -15,7 +17,7 @@ namespace Renci.SshNet.Channels internal sealed class ChannelDirectTcpip : ClientChannel, IChannelDirectTcpip { private readonly Lock _socketLock = new Lock(); - + private readonly ILogger _logger; private EventWaitHandle _channelOpen = new AutoResetEvent(initialState: false); private EventWaitHandle _channelData = new AutoResetEvent(initialState: false); private IForwardedPort _forwardedPort; @@ -31,6 +33,7 @@ internal sealed class ChannelDirectTcpip : ClientChannel, IChannelDirectTcpip public ChannelDirectTcpip(ISession session, uint localChannelNumber, uint localWindowSize, uint localPacketSize) : base(session, localChannelNumber, localWindowSize, localPacketSize) { + _logger = SshNetLoggingConfiguration.LoggerFactory.CreateLogger(); } /// @@ -157,8 +160,7 @@ private void ShutdownSocket(SocketShutdown how) } catch (SocketException ex) { - // TODO: log as warning - DiagnosticAbstraction.Log("Failure shutting down socket: " + ex); + _logger.LogInformation(ex, "Failure shutting down socket"); } } } diff --git a/src/Renci.SshNet/Channels/ChannelForwardedTcpip.cs b/src/Renci.SshNet/Channels/ChannelForwardedTcpip.cs index 2cc0c16f8..29808e2b4 100644 --- a/src/Renci.SshNet/Channels/ChannelForwardedTcpip.cs +++ b/src/Renci.SshNet/Channels/ChannelForwardedTcpip.cs @@ -5,6 +5,8 @@ using System.Threading; #endif +using Microsoft.Extensions.Logging; + using Renci.SshNet.Abstractions; using Renci.SshNet.Common; using Renci.SshNet.Messages.Connection; @@ -17,6 +19,7 @@ namespace Renci.SshNet.Channels internal sealed class ChannelForwardedTcpip : ServerChannel, IChannelForwardedTcpip { private readonly Lock _socketShutdownAndCloseLock = new Lock(); + private readonly ILogger _logger; private Socket _socket; private IForwardedPort _forwardedPort; @@ -45,6 +48,7 @@ internal ChannelForwardedTcpip(ISession session, remoteWindowSize, remotePacketSize) { + _logger = SshNetLoggingConfiguration.LoggerFactory.CreateLogger(); } /// @@ -142,8 +146,7 @@ private void ShutdownSocket(SocketShutdown how) } catch (SocketException ex) { - // TODO: log as warning - DiagnosticAbstraction.Log("Failure shutting down socket: " + ex); + _logger.LogInformation(ex, "Failure shutting down socket"); } } } diff --git a/src/Renci.SshNet/Common/Extensions.cs b/src/Renci.SshNet/Common/Extensions.cs index cf4deb077..2f3ae6ac0 100644 --- a/src/Renci.SshNet/Common/Extensions.cs +++ b/src/Renci.SshNet/Common/Extensions.cs @@ -351,5 +351,12 @@ internal static bool IsConnected(this Socket socket) return socket.Connected; } + + internal static string Join(this IEnumerable values, string separator) + { + // Used to avoid analyzers asking to "use an overload with a char parameter" + // which is not available on all targets. + return string.Join(separator, values); + } } } diff --git a/src/Renci.SshNet/Connection/ConnectorBase.cs b/src/Renci.SshNet/Connection/ConnectorBase.cs index c36fae6df..ebea9aa80 100644 --- a/src/Renci.SshNet/Connection/ConnectorBase.cs +++ b/src/Renci.SshNet/Connection/ConnectorBase.cs @@ -4,6 +4,8 @@ using System.Threading; using System.Threading.Tasks; +using Microsoft.Extensions.Logging; + using Renci.SshNet.Abstractions; using Renci.SshNet.Common; using Renci.SshNet.Messages.Transport; @@ -12,11 +14,14 @@ namespace Renci.SshNet.Connection { internal abstract class ConnectorBase : IConnector { + private readonly ILogger _logger; + protected ConnectorBase(ISocketFactory socketFactory) { ThrowHelper.ThrowIfNull(socketFactory); SocketFactory = socketFactory; + _logger = SshNetLoggingConfiguration.LoggerFactory.CreateLogger(GetType()); } internal ISocketFactory SocketFactory { get; private set; } @@ -34,7 +39,7 @@ protected ConnectorBase(ISocketFactory socketFactory) /// An error occurred trying to establish the connection. protected Socket SocketConnect(EndPoint endPoint, TimeSpan timeout) { - DiagnosticAbstraction.Log(string.Format("Initiating connection to '{0}'.", endPoint)); + _logger.LogInformation("Initiating connection to '{EndPoint}'.", endPoint); var socket = SocketFactory.Create(SocketType.Stream, ProtocolType.Tcp); @@ -65,7 +70,7 @@ protected async Task SocketConnectAsync(EndPoint endPoint, CancellationT { cancellationToken.ThrowIfCancellationRequested(); - DiagnosticAbstraction.Log(string.Format("Initiating connection to '{0}'.", endPoint)); + _logger.LogInformation("Initiating connection to '{EndPoint}'.", endPoint); var socket = SocketFactory.Create(SocketType.Stream, ProtocolType.Tcp); try diff --git a/src/Renci.SshNet/ForwardedPortDynamic.cs b/src/Renci.SshNet/ForwardedPortDynamic.cs index 7d0e5af96..5b8ff9d29 100644 --- a/src/Renci.SshNet/ForwardedPortDynamic.cs +++ b/src/Renci.SshNet/ForwardedPortDynamic.cs @@ -7,6 +7,8 @@ using System.Text; using System.Threading; +using Microsoft.Extensions.Logging; + using Renci.SshNet.Abstractions; using Renci.SshNet.Channels; using Renci.SshNet.Common; @@ -19,6 +21,7 @@ namespace Renci.SshNet /// public class ForwardedPortDynamic : ForwardedPort { + private readonly ILogger _logger; private ForwardedPortStatus _status; /// @@ -72,6 +75,7 @@ public ForwardedPortDynamic(string host, uint port) BoundHost = host; BoundPort = port; _status = ForwardedPortStatus.Stopped; + _logger = SshNetLoggingConfiguration.LoggerFactory.CreateLogger(); } /// @@ -409,8 +413,7 @@ private void InternalStop(TimeSpan timeout) if (!_pendingChannelCountdown.Wait(timeout)) { - // TODO: log as warning - DiagnosticAbstraction.Log("Timeout waiting for pending channels in dynamic forwarded port to close."); + _logger.LogInformation("Timeout waiting for pending channels in dynamic forwarded port to close."); } } diff --git a/src/Renci.SshNet/ForwardedPortLocal.cs b/src/Renci.SshNet/ForwardedPortLocal.cs index ec030be44..50d84dc74 100644 --- a/src/Renci.SshNet/ForwardedPortLocal.cs +++ b/src/Renci.SshNet/ForwardedPortLocal.cs @@ -3,7 +3,8 @@ using System.Net.Sockets; using System.Threading; -using Renci.SshNet.Abstractions; +using Microsoft.Extensions.Logging; + using Renci.SshNet.Common; namespace Renci.SshNet @@ -13,6 +14,7 @@ namespace Renci.SshNet /// public partial class ForwardedPortLocal : ForwardedPort { + private readonly ILogger _logger; private ForwardedPortStatus _status; private bool _isDisposed; private Socket _listener; @@ -101,6 +103,7 @@ public ForwardedPortLocal(string boundHost, uint boundPort, string host, uint po Host = host; Port = port; _status = ForwardedPortStatus.Stopped; + _logger = SshNetLoggingConfiguration.LoggerFactory.CreateLogger(); } /// @@ -387,8 +390,7 @@ private void InternalStop(TimeSpan timeout) if (!_pendingChannelCountdown.Wait(timeout)) { - // TODO: log as warning - DiagnosticAbstraction.Log("Timeout waiting for pending channels in local forwarded port to close."); + _logger.LogInformation("Timeout waiting for pending channels in local forwarded port to close."); } } diff --git a/src/Renci.SshNet/ForwardedPortRemote.cs b/src/Renci.SshNet/ForwardedPortRemote.cs index 07b234541..f51a3d82b 100644 --- a/src/Renci.SshNet/ForwardedPortRemote.cs +++ b/src/Renci.SshNet/ForwardedPortRemote.cs @@ -3,6 +3,8 @@ using System.Net; using System.Threading; +using Microsoft.Extensions.Logging; + using Renci.SshNet.Abstractions; using Renci.SshNet.Common; using Renci.SshNet.Messages.Connection; @@ -14,6 +16,7 @@ namespace Renci.SshNet /// public class ForwardedPortRemote : ForwardedPort { + private readonly ILogger _logger; private ForwardedPortStatus _status; private bool _requestStatus; private EventWaitHandle _globalRequestResponse = new AutoResetEvent(initialState: false); @@ -97,6 +100,7 @@ public ForwardedPortRemote(IPAddress boundHostAddress, uint boundPort, IPAddress HostAddress = hostAddress; Port = port; _status = ForwardedPortStatus.Stopped; + _logger = SshNetLoggingConfiguration.LoggerFactory.CreateLogger(); } /// @@ -208,8 +212,7 @@ protected override void StopPort(TimeSpan timeout) if (!_pendingChannelCountdown.Wait(timeout)) { - // TODO: log as warning - DiagnosticAbstraction.Log("Timeout waiting for pending channels in remote forwarded port to close."); + _logger.LogInformation("Timeout waiting for pending channels in remote forwarded port to close."); } _status = ForwardedPortStatus.Stopped; diff --git a/src/Renci.SshNet/Renci.SshNet.csproj b/src/Renci.SshNet/Renci.SshNet.csproj index d00f773fa..2936c0a7f 100644 --- a/src/Renci.SshNet/Renci.SshNet.csproj +++ b/src/Renci.SshNet/Renci.SshNet.csproj @@ -36,6 +36,7 @@ + runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/src/Renci.SshNet/Security/KeyExchange.cs b/src/Renci.SshNet/Security/KeyExchange.cs index 96eb326ca..1331098b0 100644 --- a/src/Renci.SshNet/Security/KeyExchange.cs +++ b/src/Renci.SshNet/Security/KeyExchange.cs @@ -3,7 +3,8 @@ using System.Linq; using System.Security.Cryptography; -using Renci.SshNet.Abstractions; +using Microsoft.Extensions.Logging; + using Renci.SshNet.Common; using Renci.SshNet.Compression; using Renci.SshNet.Messages; @@ -17,6 +18,7 @@ namespace Renci.SshNet.Security /// public abstract class KeyExchange : Algorithm, IKeyExchange { + private readonly ILogger _logger; private CipherInfo _clientCipherInfo; private CipherInfo _serverCipherInfo; private HashInfo _clientHashInfo; @@ -61,6 +63,11 @@ public byte[] ExchangeHash /// public event EventHandler HostKeyReceived; + private protected KeyExchange() + { + _logger = SshNetLoggingConfiguration.LoggerFactory.CreateLogger(GetType()); + } + /// public virtual void Start(Session session, KeyExchangeInitMessage message, bool sendClientInitMessage) { @@ -71,12 +78,23 @@ public virtual void Start(Session session, KeyExchangeInitMessage message, bool SendMessage(session.ClientInitMessage); } - // Determine encryption algorithm + // Determine client encryption algorithm var clientEncryptionAlgorithmName = (from b in session.ConnectionInfo.Encryptions.Keys from a in message.EncryptionAlgorithmsClientToServer where a == b select a).FirstOrDefault(); + if (_logger.IsEnabled(LogLevel.Trace)) + { + _logger.LogTrace("[{SessionId}] Encryption client to server: we offer {WeOffer}", + Session.SessionIdHex, + session.ConnectionInfo.Encryptions.Keys.Join(",")); + + _logger.LogTrace("[{SessionId}] Encryption client to server: they offer {TheyOffer}", + Session.SessionIdHex, + message.EncryptionAlgorithmsClientToServer.Join(",")); + } + if (string.IsNullOrEmpty(clientEncryptionAlgorithmName)) { throw new SshConnectionException("Client encryption algorithm not found", DisconnectReason.KeyExchangeFailed); @@ -85,11 +103,23 @@ from a in message.EncryptionAlgorithmsClientToServer session.ConnectionInfo.CurrentClientEncryption = clientEncryptionAlgorithmName; _clientCipherInfo = session.ConnectionInfo.Encryptions[clientEncryptionAlgorithmName]; - // Determine encryption algorithm + // Determine server encryption algorithm var serverDecryptionAlgorithmName = (from b in session.ConnectionInfo.Encryptions.Keys from a in message.EncryptionAlgorithmsServerToClient where a == b select a).FirstOrDefault(); + + if (_logger.IsEnabled(LogLevel.Trace)) + { + _logger.LogTrace("[{SessionId}] Encryption server to client: we offer {WeOffer}", + Session.SessionIdHex, + session.ConnectionInfo.Encryptions.Keys.Join(",")); + + _logger.LogTrace("[{SessionId}] Encryption server to client: they offer {TheyOffer}", + Session.SessionIdHex, + message.EncryptionAlgorithmsServerToClient.Join(",")); + } + if (string.IsNullOrEmpty(serverDecryptionAlgorithmName)) { throw new SshConnectionException("Server decryption algorithm not found", DisconnectReason.KeyExchangeFailed); @@ -105,6 +135,18 @@ from a in message.EncryptionAlgorithmsServerToClient from a in message.MacAlgorithmsClientToServer where a == b select a).FirstOrDefault(); + + if (_logger.IsEnabled(LogLevel.Trace)) + { + _logger.LogTrace("[{SessionId}] MAC client to server: we offer {WeOffer}", + Session.SessionIdHex, + session.ConnectionInfo.HmacAlgorithms.Keys.Join(",")); + + _logger.LogTrace("[{SessionId}] MAC client to server: they offer {TheyOffer}", + Session.SessionIdHex, + message.MacAlgorithmsClientToServer.Join(",")); + } + if (string.IsNullOrEmpty(clientHmacAlgorithmName)) { throw new SshConnectionException("Client HMAC algorithm not found", DisconnectReason.KeyExchangeFailed); @@ -121,6 +163,18 @@ from a in message.MacAlgorithmsClientToServer from a in message.MacAlgorithmsServerToClient where a == b select a).FirstOrDefault(); + + if (_logger.IsEnabled(LogLevel.Trace)) + { + _logger.LogTrace("[{SessionId}] MAC server to client: we offer {WeOffer}", + Session.SessionIdHex, + session.ConnectionInfo.HmacAlgorithms.Keys.Join(",")); + + _logger.LogTrace("[{SessionId}] MAC server to client: they offer {TheyOffer}", + Session.SessionIdHex, + message.MacAlgorithmsServerToClient.Join(",")); + } + if (string.IsNullOrEmpty(serverHmacAlgorithmName)) { throw new SshConnectionException("Server HMAC algorithm not found", DisconnectReason.KeyExchangeFailed); @@ -135,6 +189,18 @@ from a in message.MacAlgorithmsServerToClient from a in message.CompressionAlgorithmsClientToServer where a == b select a).FirstOrDefault(); + + if (_logger.IsEnabled(LogLevel.Trace)) + { + _logger.LogTrace("[{SessionId}] Compression client to server: we offer {WeOffer}", + Session.SessionIdHex, + session.ConnectionInfo.CompressionAlgorithms.Keys.Join(",")); + + _logger.LogTrace("[{SessionId}] Compression client to server: they offer {TheyOffer}", + Session.SessionIdHex, + message.CompressionAlgorithmsClientToServer.Join(",")); + } + if (string.IsNullOrEmpty(compressionAlgorithmName)) { throw new SshConnectionException("Compression algorithm not found", DisconnectReason.KeyExchangeFailed); @@ -148,6 +214,18 @@ from a in message.CompressionAlgorithmsClientToServer from a in message.CompressionAlgorithmsServerToClient where a == b select a).FirstOrDefault(); + + if (_logger.IsEnabled(LogLevel.Trace)) + { + _logger.LogTrace("[{SessionId}] Compression server to client: we offer {WeOffer}", + Session.SessionIdHex, + session.ConnectionInfo.CompressionAlgorithms.Keys.Join(",")); + + _logger.LogTrace("[{SessionId}] Compression server to client: they offer {TheyOffer}", + Session.SessionIdHex, + message.CompressionAlgorithmsServerToClient.Join(",")); + } + if (string.IsNullOrEmpty(decompressionAlgorithmName)) { throw new SshConnectionException("Decompression algorithm not found", DisconnectReason.KeyExchangeFailed); @@ -190,9 +268,9 @@ public Cipher CreateServerCipher(out bool isAead) serverKey = GenerateSessionKey(SharedKey, ExchangeHash, serverKey, _serverCipherInfo.KeySize / 8); - DiagnosticAbstraction.Log(string.Format("[{0}] Creating {1} server cipher.", - Session.ToHex(Session.SessionId), - Session.ConnectionInfo.CurrentServerEncryption)); + _logger.LogDebug("[{SessionId}] Creating {ServerEncryption} server cipher.", + Session.SessionIdHex, + Session.ConnectionInfo.CurrentServerEncryption); // Create server cipher return _serverCipherInfo.Cipher(serverKey, serverVector); @@ -218,9 +296,9 @@ public Cipher CreateClientCipher(out bool isAead) clientKey = GenerateSessionKey(SharedKey, ExchangeHash, clientKey, _clientCipherInfo.KeySize / 8); - DiagnosticAbstraction.Log(string.Format("[{0}] Creating {1} client cipher.", - Session.ToHex(Session.SessionId), - Session.ConnectionInfo.CurrentClientEncryption)); + _logger.LogDebug("[{SessionId}] Creating {ClientEncryption} client cipher.", + Session.SessionIdHex, + Session.ConnectionInfo.CurrentClientEncryption); // Create client cipher return _clientCipherInfo.Cipher(clientKey, clientVector); @@ -251,9 +329,9 @@ public HashAlgorithm CreateServerHash(out bool isEncryptThenMAC) Hash(GenerateSessionKey(SharedKey, ExchangeHash, 'F', sessionId)), _serverHashInfo.KeySize / 8); - DiagnosticAbstraction.Log(string.Format("[{0}] Creating {1} server hmac algorithm.", - Session.ToHex(Session.SessionId), - Session.ConnectionInfo.CurrentServerHmacAlgorithm)); + _logger.LogDebug("[{SessionId}] Creating {ServerHmacAlgorithm} server hmac algorithm.", + Session.SessionIdHex, + Session.ConnectionInfo.CurrentServerHmacAlgorithm); return _serverHashInfo.HashAlgorithm(serverKey); } @@ -283,9 +361,9 @@ public HashAlgorithm CreateClientHash(out bool isEncryptThenMAC) Hash(GenerateSessionKey(SharedKey, ExchangeHash, 'E', sessionId)), _clientHashInfo.KeySize / 8); - DiagnosticAbstraction.Log(string.Format("[{0}] Creating {1} client hmac algorithm.", - Session.ToHex(Session.SessionId), - Session.ConnectionInfo.CurrentClientHmacAlgorithm)); + _logger.LogDebug("[{SessionId}] Creating {ClientHmacAlgorithm} client hmac algorithm.", + Session.SessionIdHex, + Session.ConnectionInfo.CurrentClientHmacAlgorithm); return _clientHashInfo.HashAlgorithm(clientKey); } @@ -303,9 +381,9 @@ public Compressor CreateCompressor() return null; } - DiagnosticAbstraction.Log(string.Format("[{0}] Creating {1} client compressor.", - Session.ToHex(Session.SessionId), - Session.ConnectionInfo.CurrentClientCompressionAlgorithm)); + _logger.LogDebug("[{SessionId}] Creating {CompressionAlgorithm} client compressor.", + Session.SessionIdHex, + Session.ConnectionInfo.CurrentClientCompressionAlgorithm); var compressor = _compressorFactory(); @@ -327,9 +405,9 @@ public Compressor CreateDecompressor() return null; } - DiagnosticAbstraction.Log(string.Format("[{0}] Creating {1} server decompressor.", - Session.ToHex(Session.SessionId), - Session.ConnectionInfo.CurrentServerCompressionAlgorithm)); + _logger.LogDebug("[{SessionId}] Creating {ServerCompressionAlgorithm} server decompressor.", + Session.SessionIdHex, + Session.ConnectionInfo.CurrentServerCompressionAlgorithm); var decompressor = _decompressorFactory(); diff --git a/src/Renci.SshNet/ServiceFactory.cs b/src/Renci.SshNet/ServiceFactory.cs index d238b6eac..0b8e27f67 100644 --- a/src/Renci.SshNet/ServiceFactory.cs +++ b/src/Renci.SshNet/ServiceFactory.cs @@ -4,7 +4,8 @@ using System.Net.Sockets; using System.Text; -using Renci.SshNet.Abstractions; +using Microsoft.Extensions.Logging; + using Renci.SshNet.Common; using Renci.SshNet.Connection; using Renci.SshNet.Messages.Transport; @@ -25,6 +26,13 @@ internal sealed partial class ServiceFactory : IServiceFactory /// private const int PartialSuccessLimit = 5; + private readonly ILogger _logger; + + internal ServiceFactory() + { + _logger = SshNetLoggingConfiguration.LoggerFactory.CreateLogger(); + } + /// /// Creates an . /// @@ -152,7 +160,7 @@ public ISftpFileReader CreateSftpFileReader(string fileName, ISftpSession sftpSe fileSize = null; maxPendingReads = defaultMaxPendingReads; - DiagnosticAbstraction.Log(string.Format("Failed to obtain size of file. Allowing maximum {0} pending reads: {1}", maxPendingReads, ex)); + _logger.LogInformation(ex, "Failed to obtain size of file. Allowing maximum {MaxPendingReads} pending reads", maxPendingReads); } return sftpSession.CreateFileReader(handle, sftpSession, chunkSize, maxPendingReads, fileSize); diff --git a/src/Renci.SshNet/Session.cs b/src/Renci.SshNet/Session.cs index 63560b685..639a7dbba 100644 --- a/src/Renci.SshNet/Session.cs +++ b/src/Renci.SshNet/Session.cs @@ -5,10 +5,14 @@ using System.Linq; using System.Net.Sockets; using System.Security.Cryptography; +#if !NET using System.Text; +#endif using System.Threading; using System.Threading.Tasks; +using Microsoft.Extensions.Logging; + using Renci.SshNet.Abstractions; using Renci.SshNet.Channels; using Renci.SshNet.Common; @@ -75,6 +79,7 @@ public class Session : ISession /// private readonly IServiceFactory _serviceFactory; private readonly ISocketFactory _socketFactory; + private readonly ILogger _logger; /// /// Holds an object that is used to ensure only a single thread can read from @@ -288,13 +293,28 @@ public bool IsConnected } } + private byte[] _sessionId; + /// /// Gets the session id. /// /// /// The session id, or if the client has not been authenticated. /// - public byte[] SessionId { get; private set; } + public byte[] SessionId + { + get + { + return _sessionId; + } + private set + { + _sessionId = value; + SessionIdHex = ToHex(value); + } + } + + internal string SessionIdHex { get; private set; } /// /// Gets the client init message. @@ -535,6 +555,7 @@ internal Session(ConnectionInfo connectionInfo, IServiceFactory serviceFactory, ConnectionInfo = connectionInfo; _serviceFactory = serviceFactory; _socketFactory = socketFactory; + _logger = SshNetLoggingConfiguration.LoggerFactory.CreateLogger(); _messageListenerCompleted = new ManualResetEvent(initialState: true); } @@ -577,7 +598,7 @@ public void Connect() ServerVersion = ConnectionInfo.ServerVersion = serverIdentification.ToString(); ConnectionInfo.ClientVersion = ClientVersion; - DiagnosticAbstraction.Log(string.Format("Server version '{0}'.", serverIdentification)); + _logger.LogInformation("Server version '{ServerIdentification}'.", serverIdentification); if (!(serverIdentification.ProtocolVersion.Equals("2.0") || serverIdentification.ProtocolVersion.Equals("1.99"))) { @@ -703,7 +724,7 @@ public async Task ConnectAsync(CancellationToken cancellationToken) ServerVersion = ConnectionInfo.ServerVersion = serverIdentification.ToString(); ConnectionInfo.ClientVersion = ClientVersion; - DiagnosticAbstraction.Log(string.Format("Server version '{0}'.", serverIdentification)); + _logger.LogInformation("Server version '{ServerIdentification}'.", serverIdentification); if (!(serverIdentification.ProtocolVersion.Equals("2.0") || serverIdentification.ProtocolVersion.Equals("1.99"))) { @@ -796,7 +817,7 @@ public async Task ConnectAsync(CancellationToken cancellationToken) /// public void Disconnect() { - DiagnosticAbstraction.Log(string.Format("[{0}] Disconnecting session.", ToHex(SessionId))); + _logger.LogInformation("[{SessionId}] Disconnecting session.", SessionIdHex); // send SSH_MSG_DISCONNECT message, clear socket read buffer and dispose it Disconnect(DisconnectReason.ByApplication, "Connection terminated by the client."); @@ -1026,7 +1047,10 @@ internal void SendMessage(Message message) WaitOnHandle(_keyExchangeCompletedWaitHandle.WaitHandle); } - DiagnosticAbstraction.Log(string.Format("[{0}] Sending message '{1}' to server: '{2}'.", ToHex(SessionId), message.GetType().Name, message)); + if (_logger.IsEnabled(LogLevel.Trace)) + { + _logger.LogTrace("[{SessionId}] Sending message {MessageName}({MessageNumber}) to server: '{Message}'.", SessionIdHex, message.MessageName, message.MessageNumber, message.ToString()); + } var paddingMultiplier = _clientCipher is null ? (byte)8 : Math.Max((byte)8, _clientCipher.MinimumSize); var packetData = message.GetPacket(paddingMultiplier, _clientCompression, _clientEtm || _clientAead); @@ -1165,12 +1189,12 @@ private bool TrySendMessage(Message message) } catch (SshException ex) { - DiagnosticAbstraction.Log(string.Format("Failure sending message '{0}' to server: '{1}' => {2}", message.GetType().Name, message, ex)); + _logger.LogInformation(ex, "Failure sending message {MessageName}({MessageNumber}) to server: '{Message}'", message.MessageName, message.MessageNumber, message.ToString()); return false; } catch (SocketException ex) { - DiagnosticAbstraction.Log(string.Format("Failure sending message '{0}' to server: '{1}' => {2}", message.GetType().Name, message, ex)); + _logger.LogInformation(ex, "Failure sending message {MessageName}({MessageNumber}) to server: '{Message}'", message.MessageName, message.MessageNumber, message.ToString()); return false; } } @@ -1380,7 +1404,7 @@ private void TrySendDisconnect(DisconnectReason reasonCode, string message) /// message. internal void OnDisconnectReceived(DisconnectMessage message) { - DiagnosticAbstraction.Log(string.Format("[{0}] Disconnect received: {1} {2}.", ToHex(SessionId), message.ReasonCode, message.Description)); + _logger.LogInformation("[{SessionId}] Disconnect received: {ReasonCode} {MessageDescription}.", SessionIdHex, message.ReasonCode, message.Description); // transition to disconnecting state to avoid throwing exceptions while cleaning up, and to // ensure any exceptions that are raised do not overwrite the SshConnectionException that we @@ -1475,7 +1499,7 @@ internal void OnKeyExchangeInitReceived(KeyExchangeInitMessage message) { _isStrictKex = true; - DiagnosticAbstraction.Log(string.Format("[{0}] Enabling strict key exchange extension.", ToHex(SessionId))); + _logger.LogDebug("[{SessionId}] Enabling strict key exchange extension.", SessionIdHex); if (_inboundPacketSequence != 1) { @@ -1491,7 +1515,7 @@ internal void OnKeyExchangeInitReceived(KeyExchangeInitMessage message) ConnectionInfo.CurrentKeyExchangeAlgorithm = _keyExchange.Name; - DiagnosticAbstraction.Log(string.Format("[{0}] Performing {1} key exchange.", ToHex(SessionId), ConnectionInfo.CurrentKeyExchangeAlgorithm)); + _logger.LogDebug("[{SessionId}] Performing {KeyExchangeAlgorithm} key exchange.", SessionIdHex, ConnectionInfo.CurrentKeyExchangeAlgorithm); _keyExchange.HostKeyReceived += KeyExchange_HostKeyReceived; @@ -1807,34 +1831,33 @@ private Message LoadMessage(byte[] data, int offset, int count) var message = _sshMessageFactory.Create(messageType); message.Load(data, offset + 1, count - 1); - DiagnosticAbstraction.Log(string.Format("[{0}] Received message '{1}' from server: '{2}'.", ToHex(SessionId), message.GetType().Name, message)); + if (_logger.IsEnabled(LogLevel.Trace)) + { + _logger.LogTrace("[{SessionId}] Received message {MessageName}({MessageNumber}) from server: '{Message}'.", SessionIdHex, message.MessageName, message.MessageNumber, message.ToString()); + } return message; } - private static string ToHex(byte[] bytes, int offset) + private static string ToHex(byte[] bytes) { - var byteCount = bytes.Length - offset; - - var builder = new StringBuilder(bytes.Length * 2); - - for (var i = offset; i < byteCount; i++) + if (bytes is null) { - var b = bytes[i]; - _ = builder.Append(b.ToString("X2")); + return null; } - return builder.ToString(); - } +#if NET + return Convert.ToHexString(bytes); +#else + var builder = new StringBuilder(bytes.Length * 2); - internal static string ToHex(byte[] bytes) - { - if (bytes is null) + foreach (var b in bytes) { - return null; + builder.Append(b.ToString("X2")); } - return ToHex(bytes, 0); + return builder.ToString(); +#endif } /// @@ -1951,7 +1974,7 @@ private void SocketDisconnectAndDispose() { try { - DiagnosticAbstraction.Log(string.Format("[{0}] Shutting down socket.", ToHex(SessionId))); + _logger.LogDebug("[{SessionId}] Shutting down socket.", SessionIdHex); // Interrupt any pending reads; should be done outside of socket read lock as we // actually want shutdown the socket to make sure blocking reads are interrupted. @@ -1963,14 +1986,13 @@ private void SocketDisconnectAndDispose() } catch (SocketException ex) { - // TODO: log as warning - DiagnosticAbstraction.Log("Failure shutting down socket: " + ex); + _logger.LogInformation(ex, "Failure shutting down socket"); } } - DiagnosticAbstraction.Log(string.Format("[{0}] Disposing socket.", ToHex(SessionId))); + _logger.LogDebug("[{SessionId}] Disposing socket.", SessionIdHex); _socket.Dispose(); - DiagnosticAbstraction.Log(string.Format("[{0}] Disposed socket.", ToHex(SessionId))); + _logger.LogDebug("[{SessionId}] Disposed socket.", SessionIdHex); _socket = null; } } @@ -2054,7 +2076,7 @@ private void RaiseError(Exception exp) { var connectionException = exp as SshConnectionException; - DiagnosticAbstraction.Log(string.Format("[{0}] Raised exception: {1}", ToHex(SessionId), exp)); + _logger.LogInformation(exp, "[{SessionId}] Raised exception", SessionIdHex); if (_isDisconnecting) { @@ -2081,7 +2103,7 @@ private void RaiseError(Exception exp) if (connectionException != null) { - DiagnosticAbstraction.Log(string.Format("[{0}] Disconnecting after exception: {1}", ToHex(SessionId), exp)); + _logger.LogInformation(exp, "[{SessionId}] Disconnecting after exception", SessionIdHex); Disconnect(connectionException.DisconnectReason, exp.ToString()); } } @@ -2154,7 +2176,7 @@ protected virtual void Dispose(bool disposing) if (disposing) { - DiagnosticAbstraction.Log(string.Format("[{0}] Disposing session.", ToHex(SessionId))); + _logger.LogDebug("[{SessionId}] Disposing session.", SessionIdHex); Disconnect(); diff --git a/src/Renci.SshNet/Sftp/SftpFileReader.cs b/src/Renci.SshNet/Sftp/SftpFileReader.cs index 40e13edd4..3c86fc31b 100644 --- a/src/Renci.SshNet/Sftp/SftpFileReader.cs +++ b/src/Renci.SshNet/Sftp/SftpFileReader.cs @@ -4,6 +4,8 @@ using System.Runtime.ExceptionServices; using System.Threading; +using Microsoft.Extensions.Logging; + using Renci.SshNet.Abstractions; using Renci.SshNet.Common; @@ -22,6 +24,7 @@ internal sealed class SftpFileReader : ISftpFileReader private readonly ManualResetEvent _readAheadCompleted; private readonly Dictionary _queue; private readonly WaitHandle[] _waitHandles; + private readonly ILogger _logger; /// /// Holds the size of the file, when available. @@ -68,6 +71,7 @@ public SftpFileReader(byte[] handle, ISftpSession sftpSession, uint chunkSize, i _readAheadCompleted = new ManualResetEvent(initialState: false); _disposingWaitHandle = new ManualResetEvent(initialState: false); _waitHandles = _sftpSession.CreateWaitHandleArray(_disposingWaitHandle, _semaphore.AvailableWaitHandle); + _logger = SshNetLoggingConfiguration.LoggerFactory.CreateLogger(); StartReadAhead(); } @@ -266,7 +270,7 @@ private void Dispose(bool disposing) } catch (Exception ex) { - DiagnosticAbstraction.Log("Failure closing handle: " + ex); + _logger.LogInformation(ex, "Failure closing handle"); } } } diff --git a/src/Renci.SshNet/SshNetLoggingConfiguration.cs b/src/Renci.SshNet/SshNetLoggingConfiguration.cs new file mode 100644 index 000000000..4add75c9f --- /dev/null +++ b/src/Renci.SshNet/SshNetLoggingConfiguration.cs @@ -0,0 +1,26 @@ +#nullable enable +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; + +using Renci.SshNet.Common; + +namespace Renci.SshNet +{ + /// + /// Allows configuring the logging for internal logs of SSH.NET. + /// + public static class SshNetLoggingConfiguration + { + internal static ILoggerFactory LoggerFactory { get; private set; } = NullLoggerFactory.Instance; + + /// + /// Initializes the logging for SSH.NET. + /// + /// The logger factory. + public static void InitializeLogging(ILoggerFactory loggerFactory) + { + ThrowHelper.ThrowIfNull(loggerFactory); + LoggerFactory = loggerFactory; + } + } +} diff --git a/src/Renci.SshNet/SubsystemSession.cs b/src/Renci.SshNet/SubsystemSession.cs index 5ddd80718..bb3ddfc9b 100644 --- a/src/Renci.SshNet/SubsystemSession.cs +++ b/src/Renci.SshNet/SubsystemSession.cs @@ -4,7 +4,8 @@ using System.Threading; using System.Threading.Tasks; -using Renci.SshNet.Abstractions; +using Microsoft.Extensions.Logging; + using Renci.SshNet.Channels; using Renci.SshNet.Common; @@ -22,6 +23,7 @@ internal abstract class SubsystemSession : ISubsystemSession private const int SystemWaitHandleCount = 3; private readonly string _subsystemName; + private readonly ILogger _logger; private ISession _session; private IChannelSession _channel; private Exception _exception; @@ -84,6 +86,7 @@ protected SubsystemSession(ISession session, string subsystemName, int operation _session = session; _subsystemName = subsystemName; + _logger = SshNetLoggingConfiguration.LoggerFactory.CreateLogger(GetType()); OperationTimeout = operationTimeout; } @@ -180,7 +183,7 @@ protected void RaiseError(Exception error) { _exception = error; - DiagnosticAbstraction.Log("Raised exception: " + error); + _logger.LogInformation(error, "Raised exception"); _ = _errorOccuredWaitHandle?.Set(); diff --git a/test/Renci.SshNet.IntegrationTests/Renci.SshNet.IntegrationTests.csproj b/test/Renci.SshNet.IntegrationTests/Renci.SshNet.IntegrationTests.csproj index 06ae83ea6..d9f89ac45 100644 --- a/test/Renci.SshNet.IntegrationTests/Renci.SshNet.IntegrationTests.csproj +++ b/test/Renci.SshNet.IntegrationTests/Renci.SshNet.IntegrationTests.csproj @@ -18,6 +18,8 @@ runtime; build; native; contentfiles; analyzers; buildtransitive all + + diff --git a/test/Renci.SshNet.IntegrationTests/TestsFixtures/InfrastructureFixture.cs b/test/Renci.SshNet.IntegrationTests/TestsFixtures/InfrastructureFixture.cs index 4be520af6..7c095b867 100644 --- a/test/Renci.SshNet.IntegrationTests/TestsFixtures/InfrastructureFixture.cs +++ b/test/Renci.SshNet.IntegrationTests/TestsFixtures/InfrastructureFixture.cs @@ -2,23 +2,27 @@ using DotNet.Testcontainers.Containers; using DotNet.Testcontainers.Images; +using Microsoft.Extensions.Logging; + namespace Renci.SshNet.IntegrationTests.TestsFixtures { public sealed class InfrastructureFixture : IDisposable { private InfrastructureFixture() { + _loggerFactory = LoggerFactory.Create(builder => + { + builder.SetMinimumLevel(LogLevel.Debug); + builder.AddFilter("testcontainers", LogLevel.Information); + builder.AddConsole(); + }); + + SshNetLoggingConfiguration.InitializeLogging(_loggerFactory); } - private static readonly Lazy InstanceLazy = new Lazy(() => new InfrastructureFixture()); + public static InfrastructureFixture Instance { get; } = new InfrastructureFixture(); - public static InfrastructureFixture Instance - { - get - { - return InstanceLazy.Value; - } - } + private readonly ILoggerFactory _loggerFactory; private IContainer _sshServer; @@ -34,11 +38,14 @@ public static InfrastructureFixture Instance public async Task InitializeAsync() { + var containerLogger = _loggerFactory.CreateLogger("testcontainers"); + _sshServerImage = new ImageFromDockerfileBuilder() .WithName("renci-ssh-tests-server-image") .WithDockerfileDirectory(CommonDirectoryPath.GetSolutionDirectory(), Path.Combine("test", "Renci.SshNet.IntegrationTests")) .WithDockerfile("Dockerfile.TestServer") .WithDeleteIfExists(true) + .WithLogger(containerLogger) .Build(); await _sshServerImage.CreateAsync(); @@ -47,6 +54,7 @@ public async Task InitializeAsync() .WithHostname("renci-ssh-tests-server") .WithImage(_sshServerImage) .WithPortBinding(22, true) + .WithLogger(containerLogger) .Build(); await _sshServer.StartAsync(); diff --git a/test/Renci.SshNet.IntegrationTests/TestsFixtures/IntegrationTestBase.cs b/test/Renci.SshNet.IntegrationTests/TestsFixtures/IntegrationTestBase.cs index d08578e4e..6838e54d7 100644 --- a/test/Renci.SshNet.IntegrationTests/TestsFixtures/IntegrationTestBase.cs +++ b/test/Renci.SshNet.IntegrationTests/TestsFixtures/IntegrationTestBase.cs @@ -1,6 +1,4 @@ -using System.Diagnostics; - -using Renci.SshNet.Abstractions; +using Microsoft.Extensions.Logging; namespace Renci.SshNet.IntegrationTests.TestsFixtures { @@ -10,6 +8,7 @@ namespace Renci.SshNet.IntegrationTests.TestsFixtures public abstract class IntegrationTestBase { private readonly InfrastructureFixture _infrastructureFixture; + private readonly ILogger _logger; /// /// The SSH Server host name. @@ -58,13 +57,10 @@ public SshUser User protected IntegrationTestBase() { _infrastructureFixture = InfrastructureFixture.Instance; - ShowInfrastructureInformation(); - } - - private void ShowInfrastructureInformation() - { - Console.WriteLine($"SSH Server host name: {_infrastructureFixture.SshServerHostName}"); - Console.WriteLine($"SSH Server port: {_infrastructureFixture.SshServerPort}"); + _logger = SshNetLoggingConfiguration.LoggerFactory.CreateLogger(GetType()); + _logger.LogDebug("SSH Server: {Host}:{Port}", + _infrastructureFixture.SshServerHostName, + _infrastructureFixture.SshServerPort); } /// @@ -85,18 +81,5 @@ protected void CreateTestFile(string fileName, int size) } } } - - protected void EnableTracing() - { - DiagnosticAbstraction.Source.Switch = new SourceSwitch("sourceSwitch", nameof(SourceLevels.Verbose)); - DiagnosticAbstraction.Source.Listeners.Remove("Default"); - DiagnosticAbstraction.Source.Listeners.Add(new ConsoleTraceListener() { Name = "TestConsoleLogger" }); - } - - protected void DisableTracing() - { - DiagnosticAbstraction.Source.Switch = new SourceSwitch("sourceSwitch", nameof(SourceLevels.Off)); - DiagnosticAbstraction.Source.Listeners.Remove("TestConsoleLogger"); - } } } From 3b4f2cfc1c6c0da935faf1c601b20026dceda473 Mon Sep 17 00:00:00 2001 From: Scott Xu Date: Tue, 3 Dec 2024 06:44:44 +0800 Subject: [PATCH 4/6] [Private Key] Add support for PuTTY private key file format (V3 and V2) (#1543) * [Private Key] Add support for PuTTY private key * add negative test for mac --------- Co-authored-by: Rob Hague --- Directory.Packages.props | 2 +- README.md | 9 +- src/Renci.SshNet/PrivateKeyFile.PKCS1.cs | 12 +- src/Renci.SshNet/PrivateKeyFile.PuTTY.cs | 273 ++++++++++++++++++ src/Renci.SshNet/PrivateKeyFile.cs | 61 +++- .../Key.PuTTY2.Ed25519.Encrypted.12345.ppk | 9 + test/Data/Key.PuTTY2.Ed25519.ppk | 9 + test/Data/Key.PuTTY2.RSA.Encrypted.12345.ppk | 26 ++ test/Data/Key.PuTTY2.RSA.ppk | 26 ++ ...ey.PuTTY3.DSA.Encrypted.Argon2id.12345.ppk | 22 ++ test/Data/Key.PuTTY3.DSA.ppk | 17 ++ ....PuTTY3.ECDSA.Encrypted.Argon2id.12345.ppk | 15 + test/Data/Key.PuTTY3.ECDSA.ppk | 10 + ...PuTTY3.Ed25519.Encrypted.Argon2d.12345.ppk | 14 + ...PuTTY3.Ed25519.Encrypted.Argon2i.12345.ppk | 14 + ...uTTY3.Ed25519.Encrypted.Argon2id.12345.ppk | 14 + test/Data/Key.PuTTY3.Ed25519.ppk | 9 + ...ey.PuTTY3.RSA.Encrypted.Argon2id.12345.ppk | 31 ++ test/Data/Key.PuTTY3.RSA.ppk | 26 ++ .../Classes/PrivateKeyFileTest.cs | 94 ++++++ 20 files changed, 678 insertions(+), 15 deletions(-) create mode 100644 src/Renci.SshNet/PrivateKeyFile.PuTTY.cs create mode 100644 test/Data/Key.PuTTY2.Ed25519.Encrypted.12345.ppk create mode 100644 test/Data/Key.PuTTY2.Ed25519.ppk create mode 100644 test/Data/Key.PuTTY2.RSA.Encrypted.12345.ppk create mode 100644 test/Data/Key.PuTTY2.RSA.ppk create mode 100644 test/Data/Key.PuTTY3.DSA.Encrypted.Argon2id.12345.ppk create mode 100644 test/Data/Key.PuTTY3.DSA.ppk create mode 100644 test/Data/Key.PuTTY3.ECDSA.Encrypted.Argon2id.12345.ppk create mode 100644 test/Data/Key.PuTTY3.ECDSA.ppk create mode 100644 test/Data/Key.PuTTY3.Ed25519.Encrypted.Argon2d.12345.ppk create mode 100644 test/Data/Key.PuTTY3.Ed25519.Encrypted.Argon2i.12345.ppk create mode 100644 test/Data/Key.PuTTY3.Ed25519.Encrypted.Argon2id.12345.ppk create mode 100644 test/Data/Key.PuTTY3.Ed25519.ppk create mode 100644 test/Data/Key.PuTTY3.RSA.Encrypted.Argon2id.12345.ppk create mode 100644 test/Data/Key.PuTTY3.RSA.ppk diff --git a/Directory.Packages.props b/Directory.Packages.props index 3964b45d7..4352dee4d 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -5,7 +5,7 @@ - + diff --git a/README.md b/README.md index c308a6bfd..ca0e5b327 100644 --- a/README.md +++ b/README.md @@ -102,17 +102,21 @@ The main types provided by this library are: * OpenSSL PKCS#8 PEM format ("BEGIN PRIVATE KEY", "BEGIN ENCRYPTED PRIVATE KEY") * ssh.com format ("BEGIN SSH2 ENCRYPTED PRIVATE KEY") * OpenSSH key format ("BEGIN OPENSSH PRIVATE KEY") + * PuTTY private key format ("PuTTY-User-Key-File-2", "PuTTY-User-Key-File-3") * DSA in * OpenSSL traditional PEM format ("BEGIN DSA PRIVATE KEY") * OpenSSL PKCS#8 PEM format ("BEGIN PRIVATE KEY", "BEGIN ENCRYPTED PRIVATE KEY") * ssh.com format ("BEGIN SSH2 ENCRYPTED PRIVATE KEY") + * PuTTY private key format ("PuTTY-User-Key-File-2", "PuTTY-User-Key-File-3") * ECDSA 256/384/521 in * OpenSSL traditional PEM format ("BEGIN EC PRIVATE KEY") * OpenSSL PKCS#8 PEM format ("BEGIN PRIVATE KEY", "BEGIN ENCRYPTED PRIVATE KEY") * OpenSSH key format ("BEGIN OPENSSH PRIVATE KEY") + * PuTTY private key format ("PuTTY-User-Key-File-2", "PuTTY-User-Key-File-3") * ED25519 in * OpenSSL PKCS#8 PEM format ("BEGIN PRIVATE KEY", "BEGIN ENCRYPTED PRIVATE KEY") * OpenSSH key format ("BEGIN OPENSSH PRIVATE KEY") + * PuTTY private key format ("PuTTY-User-Key-File-2", "PuTTY-User-Key-File-3") Private keys in OpenSSL traditional PEM format can be encrypted using one of the following cipher methods: * DES-EDE3-CBC @@ -124,7 +128,7 @@ Private keys in OpenSSL traditional PEM format can be encrypted using one of the Private keys in OpenSSL PKCS#8 PEM format can be encrypted using any cipher method BouncyCastle supports. -Private keys in ssh.com format can be encrypted using one of the following cipher methods: +Private keys in ssh.com format can be encrypted using the following cipher method: * 3des-cbc Private keys in OpenSSH key format can be encrypted using one of the following cipher methods: @@ -139,6 +143,9 @@ Private keys in OpenSSH key format can be encrypted using one of the following c * aes256-gcm@openssh.com * chacha20-poly1305@openssh.com +Private keys in PuTTY private key format can be encrypted using the following cipher method: +* aes256-cbc + ## Host Key Algorithms **SSH.NET** supports the following host key algorithms: diff --git a/src/Renci.SshNet/PrivateKeyFile.PKCS1.cs b/src/Renci.SshNet/PrivateKeyFile.PKCS1.cs index 0ef8d4bc2..791a1fc1d 100644 --- a/src/Renci.SshNet/PrivateKeyFile.PKCS1.cs +++ b/src/Renci.SshNet/PrivateKeyFile.PKCS1.cs @@ -42,13 +42,11 @@ public Key Parse() { throw new SshPassPhraseNullOrEmptyException("Private key is encrypted but passphrase is empty."); } - - var binarySalt = new byte[_salt.Length / 2]; - for (var i = 0; i < binarySalt.Length; i++) - { - binarySalt[i] = Convert.ToByte(_salt.Substring(i * 2, 2), 16); - } - +#if NET + var binarySalt = Convert.FromHexString(_salt); +#else + var binarySalt = Org.BouncyCastle.Utilities.Encoders.Hex.Decode(_salt); +#endif CipherInfo cipher; switch (_cipherName) { diff --git a/src/Renci.SshNet/PrivateKeyFile.PuTTY.cs b/src/Renci.SshNet/PrivateKeyFile.PuTTY.cs new file mode 100644 index 000000000..627fcb991 --- /dev/null +++ b/src/Renci.SshNet/PrivateKeyFile.PuTTY.cs @@ -0,0 +1,273 @@ +#nullable enable +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Security.Cryptography; +using System.Text; + +using Org.BouncyCastle.Crypto.Generators; +using Org.BouncyCastle.Crypto.Parameters; + +using Renci.SshNet.Abstractions; +using Renci.SshNet.Common; +using Renci.SshNet.Security; +using Renci.SshNet.Security.Cryptography.Ciphers; + +namespace Renci.SshNet +{ + public partial class PrivateKeyFile + { + private sealed class PuTTY : IPrivateKeyParser + { + private readonly string _version; + private readonly string _algorithmName; + private readonly string _encryptionType; + private readonly string _comment; + private readonly byte[] _publicKey; + private readonly string? _argon2Type; + private readonly string? _argon2Salt; + private readonly string? _argon2Iterations; + private readonly string? _argon2Memory; + private readonly string? _argon2Parallelism; + private readonly byte[] _data; + private readonly string _mac; + private readonly string? _passPhrase; + + public PuTTY(string version, string algorithmName, string encryptionType, string comment, byte[] publicKey, string? argon2Type, string? argon2Salt, string? argon2Iterations, string? argon2Memory, string? argon2Parallelism, byte[] data, string mac, string? passPhrase) + { + _version = version; + _algorithmName = algorithmName; + _encryptionType = encryptionType; + _comment = comment; + _publicKey = publicKey; + _argon2Type = argon2Type; + _argon2Salt = argon2Salt; + _argon2Iterations = argon2Iterations; + _argon2Memory = argon2Memory; + _argon2Parallelism = argon2Parallelism; + _data = data; + _mac = mac; + _passPhrase = passPhrase; + } + + /// + /// Parses an PuTTY PPK key file. + /// . + /// + public Key Parse() + { + byte[] privateKey; + HMAC hmac; + switch (_encryptionType) + { + case "aes256-cbc": + if (string.IsNullOrEmpty(_passPhrase)) + { + throw new SshPassPhraseNullOrEmptyException("Private key is encrypted but passphrase is empty."); + } + + byte[] cipherKey; + byte[] cipherIV; + switch (_version) + { + case "3": + ThrowHelper.ThrowIfNullOrEmpty(_argon2Type); + ThrowHelper.ThrowIfNullOrEmpty(_argon2Iterations); + ThrowHelper.ThrowIfNullOrEmpty(_argon2Memory); + ThrowHelper.ThrowIfNullOrEmpty(_argon2Parallelism); + ThrowHelper.ThrowIfNullOrEmpty(_argon2Salt); + + var keyData = Argon2( + _argon2Type, + Convert.ToInt32(_argon2Iterations), + Convert.ToInt32(_argon2Memory), + Convert.ToInt32(_argon2Parallelism), +#if NET + Convert.FromHexString(_argon2Salt), +#else + Org.BouncyCastle.Utilities.Encoders.Hex.Decode(_argon2Salt), +#endif + _passPhrase); + + cipherKey = keyData.Take(32); + cipherIV = keyData.Take(32, 16); + + var macKey = keyData.Take(48, 32); + hmac = new HMACSHA256(macKey); + + break; + case "2": + keyData = V2KDF(_passPhrase); + + cipherKey = keyData.Take(32); + cipherIV = new byte[16]; + + macKey = CryptoAbstraction.HashSHA1(Encoding.UTF8.GetBytes("putty-private-key-file-mac-key" + _passPhrase)).Take(20); + hmac = new HMACSHA1(macKey); + + break; + default: + throw new SshException("PuTTY key file version " + _version + " is not supported"); + } + + using (var cipher = new AesCipher(cipherKey, cipherIV, AesCipherMode.CBC, pkcs7Padding: false)) + { + privateKey = cipher.Decrypt(_data); + } + + break; + case "none": + switch (_version) + { + case "3": + hmac = new HMACSHA256(Array.Empty()); + break; + case "2": + var macKey = CryptoAbstraction.HashSHA1(Encoding.UTF8.GetBytes("putty-private-key-file-mac-key")); + hmac = new HMACSHA1(macKey); + break; + default: + throw new SshException("PuTTY key file version " + _version + " is not supported"); + } + + privateKey = _data; + break; + default: + throw new SshException("Encryption " + _encryptionType + " is not supported for PuTTY key file"); + } + + byte[] macData; + using (var macStream = new SshDataStream(256)) + { + macStream.Write(_algorithmName, Encoding.UTF8); + macStream.Write(_encryptionType, Encoding.UTF8); + macStream.Write(_comment, Encoding.UTF8); + macStream.WriteBinary(_publicKey); + macStream.WriteBinary(privateKey); + macData = macStream.ToArray(); + } + + byte[] macValue; + using (hmac) + { + macValue = hmac.ComputeHash(macData); + } +#if NET + var reference = Convert.FromHexString(_mac); +#else + var reference = Org.BouncyCastle.Utilities.Encoders.Hex.Decode(_mac); +#endif + if (!macValue.SequenceEqual(reference)) + { + throw new SshException("MAC verification failed for PuTTY key file"); + } + + var publicKeyReader = new SshDataReader(_publicKey); + var keyType = publicKeyReader.ReadString(Encoding.UTF8); + Debug.Assert(keyType == _algorithmName, $"{nameof(keyType)} is not the same as {nameof(_algorithmName)}"); + + var privateKeyReader = new SshDataReader(privateKey); + + Key parsedKey; + + switch (keyType) + { + case "ssh-ed25519": + parsedKey = new ED25519Key(privateKeyReader.ReadBignum2()); + break; + case "ecdsa-sha2-nistp256": + case "ecdsa-sha2-nistp384": + case "ecdsa-sha2-nistp521": + var curve = publicKeyReader.ReadString(Encoding.ASCII); + var pub = publicKeyReader.ReadBignum2(); + var prv = privateKeyReader.ReadBignum2(); + parsedKey = new EcdsaKey(curve, pub, prv); + break; + case "ssh-dss": + var p = publicKeyReader.ReadBignum(); + var q = publicKeyReader.ReadBignum(); + var g = publicKeyReader.ReadBignum(); + var y = publicKeyReader.ReadBignum(); + var x = privateKeyReader.ReadBignum(); + parsedKey = new DsaKey(p, q, g, y, x); + break; + case "ssh-rsa": + var exponent = publicKeyReader.ReadBignum(); // e + var modulus = publicKeyReader.ReadBignum(); // n + var d = privateKeyReader.ReadBignum(); // d + p = privateKeyReader.ReadBignum(); // p + q = privateKeyReader.ReadBignum(); // q + var inverseQ = privateKeyReader.ReadBignum(); // iqmp + parsedKey = new RsaKey(modulus, exponent, d, p, q, inverseQ); + break; + default: + throw new SshException("Key type " + keyType + " is not supported for PuTTY key file"); + } + + parsedKey.Comment = _comment; + return parsedKey; + } + + private static byte[] Argon2(string type, int iterations, int memory, int parallelism, byte[] salt, string passPhrase) + { + int param; + switch (type) + { + case "Argon2i": + param = Argon2Parameters.Argon2i; + break; + case "Argon2d": + param = Argon2Parameters.Argon2d; + break; + case "Argon2id": + param = Argon2Parameters.Argon2id; + break; + default: + throw new SshException("KDF " + type + " is not supported for PuTTY key file"); + } + + var a2p = new Argon2Parameters.Builder(param) + .WithVersion(Argon2Parameters.Version13) + .WithIterations(iterations) + .WithMemoryAsKB(memory) + .WithParallelism(parallelism) + .WithSalt(salt).Build(); + + var generator = new Argon2BytesGenerator(); + + generator.Init(a2p); + + var output = new byte[80]; + var bytes = generator.GenerateBytes(passPhrase.ToCharArray(), output); + + if (bytes != output.Length) + { + throw new SshException("Failed to generate key via Argon2"); + } + + return output; + } + + private static byte[] V2KDF(string passPhrase) + { + var cipherKey = new List(); + + var passPhraseBytes = Encoding.UTF8.GetBytes(passPhrase); + for (var sequenceNumber = 0; sequenceNumber < 2; sequenceNumber++) + { + using (var sha1 = SHA1.Create()) + { + var sequence = new byte[] { 0, 0, 0, (byte)sequenceNumber }; + _ = sha1.TransformBlock(sequence, 0, 4, outputBuffer: null, 0); + _ = sha1.TransformFinalBlock(passPhraseBytes, 0, passPhraseBytes.Length); + Debug.Assert(sha1.Hash != null, "Hash is null"); + cipherKey.AddRange(sha1.Hash); + } + } + + return cipherKey.ToArray(); + } + } + } +} diff --git a/src/Renci.SshNet/PrivateKeyFile.cs b/src/Renci.SshNet/PrivateKeyFile.cs index 8112d207c..bfbe89fd1 100644 --- a/src/Renci.SshNet/PrivateKeyFile.cs +++ b/src/Renci.SshNet/PrivateKeyFile.cs @@ -25,16 +25,16 @@ namespace Renci.SshNet /// The following private keys are supported: /// /// - /// RSA in OpenSSL PEM, ssh.com and OpenSSH key format + /// RSA in OpenSSL PEM, ssh.com, OpenSSH and PuTTY key format /// /// - /// DSA in OpenSSL PEM and ssh.com format + /// DSA in OpenSSL PEM, ssh.com and PuTTY key format /// /// - /// ECDSA 256/384/521 in OpenSSL PEM and OpenSSH key format + /// ECDSA 256/384/521 in OpenSSL PEM, OpenSSH and PuTTY key format /// /// - /// ED25519 in OpenSSL PEM and OpenSSH key format + /// ED25519 in OpenSSL PEM, OpenSSH and PuTTY key format /// /// /// @@ -73,7 +73,7 @@ namespace Renci.SshNet /// /// /// - /// The following encryption algorithms are supported for OpenSSH format: + /// The following encryption algorithms are supported for OpenSSH key format: /// /// /// 3des-cbc @@ -107,23 +107,37 @@ namespace Renci.SshNet /// /// /// + /// + /// The following encryption algorithms are supported for PuTTY key format: + /// + /// + /// aes256-cbc + /// + /// + /// /// public partial class PrivateKeyFile : IPrivateKeySource, IDisposable { private const string PrivateKeyPattern = @"^-+ *BEGIN (?\w+( \w+)*) *-+\r?\n((Proc-Type: 4,ENCRYPTED\r?\nDEK-Info: (?[A-Z0-9-]+),(?[a-fA-F0-9]+)\r?\n\r?\n)|(Comment: ""?[^\r\n]*""?\r?\n))?(?([a-zA-Z0-9/+=]{1,80}\r?\n)+)(\r?\n)?-+ *END \k *-+"; + private const string PuTTYPrivateKeyPattern = @"^(?PuTTY-User-Key-File)-(?\d+): (?[\w-]+)\r?\nEncryption: (?[\w-]+)\r?\nComment: (?.*)\r?\nPublic-Lines: \d+\r?\n(?(([a-zA-Z0-9/+=]{1,64})\r?\n)+)(Key-Derivation: (?\w+)\r?\nArgon2-Memory: (?\d+)\r?\nArgon2-Passes: (?\d+)\r?\nArgon2-Parallelism: (?\d+)\r?\nArgon2-Salt: (?[a-fA-F0-9]+)\r?\n)?Private-Lines: \d+\r?\n(?(([a-zA-Z0-9/+=]{1,64})\r?\n)+)+Private-MAC: (?[a-fA-F0-9]+)"; private const string CertificatePattern = @"(?[-\w]+@openssh\.com)\s(?[a-zA-Z0-9\/+=]*)(\s+(?.*))?"; #if NET7_0_OR_GREATER private static readonly Regex PrivateKeyRegex = GetPrivateKeyRegex(); + private static readonly Regex PuTTYPrivateKeyRegex = GetPrivateKeyPuTTYRegex(); private static readonly Regex CertificateRegex = GetCertificateRegex(); [GeneratedRegex(PrivateKeyPattern, RegexOptions.Multiline | RegexOptions.ExplicitCapture)] private static partial Regex GetPrivateKeyRegex(); + [GeneratedRegex(PuTTYPrivateKeyPattern, RegexOptions.Multiline | RegexOptions.ExplicitCapture)] + private static partial Regex GetPrivateKeyPuTTYRegex(); + [GeneratedRegex(CertificatePattern, RegexOptions.ExplicitCapture)] private static partial Regex GetCertificateRegex(); #else private static readonly Regex PrivateKeyRegex = new Regex(PrivateKeyPattern, RegexOptions.Compiled | RegexOptions.Multiline | RegexOptions.ExplicitCapture); + private static readonly Regex PuTTYPrivateKeyRegex = new Regex(PuTTYPrivateKeyPattern, RegexOptions.Compiled | RegexOptions.Multiline | RegexOptions.ExplicitCapture); private static readonly Regex CertificateRegex = new Regex(CertificatePattern, RegexOptions.Compiled | RegexOptions.ExplicitCapture); #endif @@ -287,7 +301,14 @@ private void Open(Stream privateKey, string? passPhrase) using (var sr = new StreamReader(privateKey)) { var text = sr.ReadToEnd(); - privateKeyMatch = PrivateKeyRegex.Match(text); + if (text.StartsWith("PuTTY-User-Key-File", StringComparison.Ordinal)) + { + privateKeyMatch = PuTTYPrivateKeyRegex.Match(text); + } + else + { + privateKeyMatch = PrivateKeyRegex.Match(text); + } } if (!privateKeyMatch.Success) @@ -321,6 +342,34 @@ private void Open(Stream privateKey, string? passPhrase) case "SSH2 ENCRYPTED PRIVATE KEY": parser = new SSHCOM(binaryData, passPhrase); break; + case "PuTTY-User-Key-File": + var version = privateKeyMatch.Result("${version}"); + var algorithmName = privateKeyMatch.Result("${algorithmName}"); + var encryptionType = privateKeyMatch.Result("${encryptionType}"); + var comment = privateKeyMatch.Result("${comment}"); + var publicKey = privateKeyMatch.Result("${publicKey}"); + var argon2Type = privateKeyMatch.Result("${argon2Type}"); + var argon2Memory = privateKeyMatch.Result("${argon2Memory}"); + var argon2Passes = privateKeyMatch.Result("${argon2Passes}"); + var argon2Parallelism = privateKeyMatch.Result("${argon2Parallelism}"); + var argon2Salt = privateKeyMatch.Result("${argon2Salt}"); + var mac = privateKeyMatch.Result("${mac}"); + + parser = new PuTTY( + version, + algorithmName, + encryptionType, + comment, + Convert.FromBase64String(publicKey), + argon2Type, + argon2Salt, + argon2Passes, + argon2Memory, + argon2Parallelism, + binaryData, + mac, + passPhrase); + break; default: throw new NotSupportedException(string.Format(CultureInfo.CurrentCulture, "Key '{0}' is not supported.", keyName)); } diff --git a/test/Data/Key.PuTTY2.Ed25519.Encrypted.12345.ppk b/test/Data/Key.PuTTY2.Ed25519.Encrypted.12345.ppk new file mode 100644 index 000000000..fc165774d --- /dev/null +++ b/test/Data/Key.PuTTY2.Ed25519.Encrypted.12345.ppk @@ -0,0 +1,9 @@ +PuTTY-User-Key-File-2: ssh-ed25519 +Encryption: aes256-cbc +Comment: Key.OPENSSH.ED25519 +Public-Lines: 2 +AAAAC3NzaC1lZDI1NTE5AAAAIA0JZnDQrxQZcNALfZYG7LPAW1MYEGvVW5nje7Ol +MGMi +Private-Lines: 1 +f3N2/AkwbgVeXdK155h+JCbWgBXyEk3qEyx+ChUqm4tOUQGiJ95/mTo4RbIjWn+2 +Private-MAC: 823817b8364ce7f52e278e252fd10e2f51ac8554 diff --git a/test/Data/Key.PuTTY2.Ed25519.ppk b/test/Data/Key.PuTTY2.Ed25519.ppk new file mode 100644 index 000000000..63c6b7eb7 --- /dev/null +++ b/test/Data/Key.PuTTY2.Ed25519.ppk @@ -0,0 +1,9 @@ +PuTTY-User-Key-File-2: ssh-ed25519 +Encryption: none +Comment: Key.OPENSSH.ED25519 +Public-Lines: 2 +AAAAC3NzaC1lZDI1NTE5AAAAIA0JZnDQrxQZcNALfZYG7LPAW1MYEGvVW5nje7Ol +MGMi +Private-Lines: 1 +AAAAIADMEXUw9TGuz7JykmHbzPOj8XebpZwo76iuxJtHkvAp +Private-MAC: 6a1329739932b5caaaf48d7fcd61392d498b8f5f diff --git a/test/Data/Key.PuTTY2.RSA.Encrypted.12345.ppk b/test/Data/Key.PuTTY2.RSA.Encrypted.12345.ppk new file mode 100644 index 000000000..b9c67d4db --- /dev/null +++ b/test/Data/Key.PuTTY2.RSA.Encrypted.12345.ppk @@ -0,0 +1,26 @@ +PuTTY-User-Key-File-2: ssh-rsa +Encryption: aes256-cbc +Comment: Key.OPENSSH.RSA +Public-Lines: 6 +AAAAB3NzaC1yc2EAAAADAQABAAABAQDtbs6KCLsePWaxraXweKYs/NqBWYT8Kx4w +oJHE8xO1ZO+hl0y3uF+S2FYDuHbRruhJJ4fa3sWp46lU0YVi9FXcFVawpkkxFx0m +JMJkCMffytiT3Re9neYqso3/d9xCyHg6I+dapPodKqDXiiJXxQ+1TCcTrmyRZLG/ +G34QuVWkKobm8TY78Y0MpATsXNi3q9CKEwVIAEGqO9q7SaNfTTYpiIIyvq+CXxdi +QMDifn4nJBJDHOed+sv3dmhqq6NE/ZtPlSFeBvOvwcXC6pAa9REQJlNMjwGK//q0 +4if3HaERo3q/EMu1dz30TZ3o1bpx2uLBoYUniOBVYMTmZTTTpd09 +Private-Lines: 14 +i38C+M7U/qOvzvOdtEC60v3cn354TMdDpAVk22UuNgQY7u6avW907mUuXjqU7Tjy +G66uQ+6vOnaQR6xTYWn3uI1YMWGsRylLiNZ/O/dIpITv+9xuU5U8qB+nFIH3iD6U +cBuumRKH2k2IxYVBO1nKTt3T3HejIIy3JlFifs9ylahgPD1m8jIfNARnYB09fOQr +g+nV4YNRntOqpf+cUpc+TGnz35oa9O+6fzJY/2hdkOJ+fUBdlYmcKEZZ9PgYHAlM ++aoQhL1vRoqZqzfHZYcMUBlmbRMgWiPfFB59nU0QbZ2uClbFxGGehBnCAsnd8Nni +TafVYckp3kCNLmctrmorsRgM/IXWcdqmrGJO5sHEsutQqs9Dp3KM9xhRP23xoGnZ +ssNw1qEAOP/k9JTCID/oeRE7RvAEMEdCs3l6FHKDvInR5xDuNrGehHdc3vGqklkC +1kBER2vWRX+LANNXcfISRDNajC6XKsDo6aGJRPTMj/tbVJNbjP4Oi6fwcFizV9Z0 +yJiw1yV5AjUc8nG3cuTb1wNZ8DTPccq+W6ir8U+Vmdc67Mhxas6e2UKa5nXC+HqC +jtM1oCPpxNyRoJSuxcWrwcs/yEhFfSOBU9PFsbJcX5i+ilZ2ZSipFc2fOcSx/6RC +U36mELhmHPINoTDOc6qvL/fXRkqtoQGZm2UHoINVLkeMu3ERkxxDk9kFqRcSdFd+ +nSMJt9d3YRSsjhzvro65ugKiYBubugon24frB3nARI9zoe0BZB9H/RW3RPlMT8d9 +H+gElqyiYWZKKJs+Qht6NY0Hv1gp9erMIcTTts/w7ZRc5oS8pLCKOuB9wKYepJm7 +5I0x8qVotiHglDJ7PPbP+3UzrJvGp2nwPzj4qokCZKFhPYNYiCY16V0LaBRoE3mQ +Private-MAC: 191817999553b67a7516223647e90e0b17db8918 diff --git a/test/Data/Key.PuTTY2.RSA.ppk b/test/Data/Key.PuTTY2.RSA.ppk new file mode 100644 index 000000000..33d71db93 --- /dev/null +++ b/test/Data/Key.PuTTY2.RSA.ppk @@ -0,0 +1,26 @@ +PuTTY-User-Key-File-2: ssh-rsa +Encryption: none +Comment: Key.OPENSSH.RSA +Public-Lines: 6 +AAAAB3NzaC1yc2EAAAADAQABAAABAQDtbs6KCLsePWaxraXweKYs/NqBWYT8Kx4w +oJHE8xO1ZO+hl0y3uF+S2FYDuHbRruhJJ4fa3sWp46lU0YVi9FXcFVawpkkxFx0m +JMJkCMffytiT3Re9neYqso3/d9xCyHg6I+dapPodKqDXiiJXxQ+1TCcTrmyRZLG/ +G34QuVWkKobm8TY78Y0MpATsXNi3q9CKEwVIAEGqO9q7SaNfTTYpiIIyvq+CXxdi +QMDifn4nJBJDHOed+sv3dmhqq6NE/ZtPlSFeBvOvwcXC6pAa9REQJlNMjwGK//q0 +4if3HaERo3q/EMu1dz30TZ3o1bpx2uLBoYUniOBVYMTmZTTTpd09 +Private-Lines: 14 +AAABAQDpeCr6CmnM632eu2zPkCN/W0eVJ6yftdpi4JFWA9veY5lK4RbcFR1NrRKv +Z+TWfNIGlSt+qc3eJ3IraDdsPWxsFEOBQpH4Bo1wI3dOnF/GDJV4mFAu8SQR2i/N +BFR/CtdF/GYTeOREZ9Vu/HKWsbynfnFyZfJ16XjqvaLx2PyAhje0qnREy9nhmU1u +FYc93k7HIdYv17eBs5LIjKNCBMpl7OHMStL9f8on9dirPIECo2pnZGDWQqIdGUdL +ooQja3IXBh+H5Fvov3FyHVKo61CFNaKubFLbl2kYPaOBqVd7KLDw+a6pOJYKpSZQ +zHox0Xe0WyKuvngrhAD2Sox5pEu1AAAAgQD+dPDqesFjwMJ9SXwWbqkLY3H5yXje +DZGEAXcm59L1buVHcqkkC2vIZQM0ToQPqib65bGYDPYfAsi08ropvJYpGR6HMDtd +8wU3VWkPHNpSb39rl0yFzWR7HkuyE5HwYjtYUgeM/EQ5Dq9+Zhn3W8iSBQMBWReF +7PFp0BfrxxGnawAAAIEA7t9vXgsFRX/YNMzR32bt9adFrRK3LEb+e36vlKD7aL/J +8VBe9aDlnuSkhpxrTCAiN9ZAbT4VG73zprqja4CQY4I2z0JotMUgBOS90LhCkTY5 +WhN/1mnSgcM4SQ7WrrmJNYn5K3QFaeu18kOabsrhoFWkATT268QPYNSG8ni+P/cA +AACBALFEE9FIau5dLoE3eGPfPWx+nltH6Jdtf5uwec5CUHqTWnVD07NfPLr7+Ip1 +vJ9jt0Qmp11h2XwidQLEfzBBFtgukA7b6ilx2831kJQmElcQdewo1ESmvHzWiAJP +fM4JjTcDudzQZXsq1IT4L5t8bewAoKc12OUcDSS/P2tFjpoM +Private-MAC: 7f487d19cb5d03257c9b9a2e1c7192a5d2396af5 diff --git a/test/Data/Key.PuTTY3.DSA.Encrypted.Argon2id.12345.ppk b/test/Data/Key.PuTTY3.DSA.Encrypted.Argon2id.12345.ppk new file mode 100644 index 000000000..a6205ded4 --- /dev/null +++ b/test/Data/Key.PuTTY3.DSA.Encrypted.Argon2id.12345.ppk @@ -0,0 +1,22 @@ +PuTTY-User-Key-File-3: ssh-dss +Encryption: aes256-cbc +Comment: imported-openssh-key +Public-Lines: 10 +AAAAB3NzaC1kc3MAAACBALVl3fae2O4qwsAK95SUShX0KMUNP+yl/uT3lGH9T/Zp +tnHSlrTxnTWXCl0g91KEeCaEnDDhLxm4aCv1Ag4B/yvcM4u34qkmaNLy2LiAxiqd +obZcNG61Pqwqd5IDkp38LBsn8tmb12xu9NalpUfOiSEB1cyCr4zFZMrm0wtdyJQV +AAAAFQCu+iNkqf/YOAYjYrHSCHFmWAfEYQAAAIAOVJ434UAR3Hn6lA5nWNfFOuUV +H3W7nJaP0FQJiIPx7GUbdxO9qtDNTbWkWL3c9qx5+B7Ole4xM7cvyXPrNQUYDHCF +lS+Ue2x3IeJrkdfZkH9ePP25y5A0J4/c+8XXvQaj4zA5nfw13oy5Ptyd7d3Kq5tE +DM8KiVdIhwkXjUA3PQAAAIEAm8IGZQatS7M6AfNITNWG4TI7Z2aRQjLb9/MWJIID +7c/VQ4zdTZdG3kpk0Gj9n4xreopK5NmYAdj8rtFfPBgmXltsLqt+bBcXkpxW//7W +C29WOXW3t90ySTh+cWuWfr9fV7mf4Ql/6u/ZIgpQNvnNYezazt3fK8EXjI1dAXEu +QxE= +Key-Derivation: Argon2id +Argon2-Memory: 8192 +Argon2-Passes: 8 +Argon2-Parallelism: 1 +Argon2-Salt: 310d916da49faba22ba8d2745777e5c5 +Private-Lines: 1 +xMm0Tg+o7Yq6lAs6L33y2fy3fiDPl6p71iKxm8OAgj4= +Private-MAC: fc2aef48bf90b80b97d06c32c37491db614331c2551ab37865d8719ee6cb5f4f diff --git a/test/Data/Key.PuTTY3.DSA.ppk b/test/Data/Key.PuTTY3.DSA.ppk new file mode 100644 index 000000000..abecad674 --- /dev/null +++ b/test/Data/Key.PuTTY3.DSA.ppk @@ -0,0 +1,17 @@ +PuTTY-User-Key-File-3: ssh-dss +Encryption: none +Comment: imported-openssh-key +Public-Lines: 10 +AAAAB3NzaC1kc3MAAACBALVl3fae2O4qwsAK95SUShX0KMUNP+yl/uT3lGH9T/Zp +tnHSlrTxnTWXCl0g91KEeCaEnDDhLxm4aCv1Ag4B/yvcM4u34qkmaNLy2LiAxiqd +obZcNG61Pqwqd5IDkp38LBsn8tmb12xu9NalpUfOiSEB1cyCr4zFZMrm0wtdyJQV +AAAAFQCu+iNkqf/YOAYjYrHSCHFmWAfEYQAAAIAOVJ434UAR3Hn6lA5nWNfFOuUV +H3W7nJaP0FQJiIPx7GUbdxO9qtDNTbWkWL3c9qx5+B7Ole4xM7cvyXPrNQUYDHCF +lS+Ue2x3IeJrkdfZkH9ePP25y5A0J4/c+8XXvQaj4zA5nfw13oy5Ptyd7d3Kq5tE +DM8KiVdIhwkXjUA3PQAAAIEAm8IGZQatS7M6AfNITNWG4TI7Z2aRQjLb9/MWJIID +7c/VQ4zdTZdG3kpk0Gj9n4xreopK5NmYAdj8rtFfPBgmXltsLqt+bBcXkpxW//7W +C29WOXW3t90ySTh+cWuWfr9fV7mf4Ql/6u/ZIgpQNvnNYezazt3fK8EXjI1dAXEu +QxE= +Private-Lines: 1 +AAAAFBhGOzk+Aimeob964E8+HsQNlyde +Private-MAC: 6c517ac5ede72c006b0115dd9d0830c8e699a1f4d72c708d41f68b6263d974ae diff --git a/test/Data/Key.PuTTY3.ECDSA.Encrypted.Argon2id.12345.ppk b/test/Data/Key.PuTTY3.ECDSA.Encrypted.Argon2id.12345.ppk new file mode 100644 index 000000000..1ceb26f3f --- /dev/null +++ b/test/Data/Key.PuTTY3.ECDSA.Encrypted.Argon2id.12345.ppk @@ -0,0 +1,15 @@ +PuTTY-User-Key-File-3: ecdsa-sha2-nistp256 +Encryption: aes256-cbc +Comment: imported-openssh-key +Public-Lines: 3 +AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBEA+TDv5/cqk +g07M8M1aQKS8eUkBXnBOWXw5IMalXR0HnJtQQD6M2eHihjYSp+9oU+/Zi5afR11/ +qDRHLlU/Nx8= +Key-Derivation: Argon2id +Argon2-Memory: 8192 +Argon2-Passes: 8 +Argon2-Parallelism: 1 +Argon2-Salt: d496ed94c393d7c5df6ecd10440210ba +Private-Lines: 1 +7gcJBC98J9hOJVkp0rUJrx8vlWIklCf+/7iQZT+1pCyhaWs+5jm/dIw8aCGMhM0a +Private-MAC: 916979ee4696a5ee7674816259b2ac32c5258eaec94c554a9bc8d090dbc45b50 diff --git a/test/Data/Key.PuTTY3.ECDSA.ppk b/test/Data/Key.PuTTY3.ECDSA.ppk new file mode 100644 index 000000000..1cee78214 --- /dev/null +++ b/test/Data/Key.PuTTY3.ECDSA.ppk @@ -0,0 +1,10 @@ +PuTTY-User-Key-File-3: ecdsa-sha2-nistp256 +Encryption: none +Comment: imported-openssh-key +Public-Lines: 3 +AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBEA+TDv5/cqk +g07M8M1aQKS8eUkBXnBOWXw5IMalXR0HnJtQQD6M2eHihjYSp+9oU+/Zi5afR11/ +qDRHLlU/Nx8= +Private-Lines: 1 +AAAAIEdqaFKgJBIibVjyUh1v7Y35LwIQJrocdTaYFLwl7iB0 +Private-MAC: 3df7a232c8e3021a8f9809968b748c9db46ad3c668fa7d3f4d87ec8cbd838522 diff --git a/test/Data/Key.PuTTY3.Ed25519.Encrypted.Argon2d.12345.ppk b/test/Data/Key.PuTTY3.Ed25519.Encrypted.Argon2d.12345.ppk new file mode 100644 index 000000000..d48923d9e --- /dev/null +++ b/test/Data/Key.PuTTY3.Ed25519.Encrypted.Argon2d.12345.ppk @@ -0,0 +1,14 @@ +PuTTY-User-Key-File-3: ssh-ed25519 +Encryption: aes256-cbc +Comment: Key.OPENSSH.ED25519 +Public-Lines: 2 +AAAAC3NzaC1lZDI1NTE5AAAAIA0JZnDQrxQZcNALfZYG7LPAW1MYEGvVW5nje7Ol +MGMi +Key-Derivation: Argon2d +Argon2-Memory: 8192 +Argon2-Passes: 8 +Argon2-Parallelism: 1 +Argon2-Salt: a937df822b7304499c58f1795e90376a +Private-Lines: 1 +EjcQkrXqEA3uJb/TfLt8MM+Gt1pM80S5fn5AlUWiUdhvrCG7pTTSVpaYYP8svW89 +Private-MAC: 673bd514209afa19d14506300f236a6049a915e7c7ec1a67d8df464e948f2654 diff --git a/test/Data/Key.PuTTY3.Ed25519.Encrypted.Argon2i.12345.ppk b/test/Data/Key.PuTTY3.Ed25519.Encrypted.Argon2i.12345.ppk new file mode 100644 index 000000000..85e539757 --- /dev/null +++ b/test/Data/Key.PuTTY3.Ed25519.Encrypted.Argon2i.12345.ppk @@ -0,0 +1,14 @@ +PuTTY-User-Key-File-3: ssh-ed25519 +Encryption: aes256-cbc +Comment: Key.OPENSSH.ED25519 +Public-Lines: 2 +AAAAC3NzaC1lZDI1NTE5AAAAIA0JZnDQrxQZcNALfZYG7LPAW1MYEGvVW5nje7Ol +MGMi +Key-Derivation: Argon2i +Argon2-Memory: 8192 +Argon2-Passes: 8 +Argon2-Parallelism: 1 +Argon2-Salt: bf99b3e5c7d28d566c8c759d8a1a41d2 +Private-Lines: 1 +tbSkuSPLzdnnQuGYnC/L1fv9UnFzCBkwtk5aLDZYJFiD08h5EmpGCfJJHe0lJx9/ +Private-MAC: eee47ba955ba32fc85e3eb7a70d7e5f0359eacbf9f1abd5f02904fa98db513b7 diff --git a/test/Data/Key.PuTTY3.Ed25519.Encrypted.Argon2id.12345.ppk b/test/Data/Key.PuTTY3.Ed25519.Encrypted.Argon2id.12345.ppk new file mode 100644 index 000000000..a7b05be4c --- /dev/null +++ b/test/Data/Key.PuTTY3.Ed25519.Encrypted.Argon2id.12345.ppk @@ -0,0 +1,14 @@ +PuTTY-User-Key-File-3: ssh-ed25519 +Encryption: aes256-cbc +Comment: Key.OPENSSH.ED25519 +Public-Lines: 2 +AAAAC3NzaC1lZDI1NTE5AAAAIA0JZnDQrxQZcNALfZYG7LPAW1MYEGvVW5nje7Ol +MGMi +Key-Derivation: Argon2id +Argon2-Memory: 8192 +Argon2-Passes: 21 +Argon2-Parallelism: 1 +Argon2-Salt: a8bb359d93fc58732e595da9c80b2e88 +Private-Lines: 1 +GHfMNhx9V3k/OKJxNb6QJ93Gwg9/slksETZ0Ns0edelE6hJfUc+XhXnQBLkUHZ6H +Private-MAC: b07d3ddb544a7396637bc607bedd3d14ed28db929f2bdb5af4a94b410c3d9feb diff --git a/test/Data/Key.PuTTY3.Ed25519.ppk b/test/Data/Key.PuTTY3.Ed25519.ppk new file mode 100644 index 000000000..74a330225 --- /dev/null +++ b/test/Data/Key.PuTTY3.Ed25519.ppk @@ -0,0 +1,9 @@ +PuTTY-User-Key-File-3: ssh-ed25519 +Encryption: none +Comment: Key.OPENSSH.ED25519 +Public-Lines: 2 +AAAAC3NzaC1lZDI1NTE5AAAAIA0JZnDQrxQZcNALfZYG7LPAW1MYEGvVW5nje7Ol +MGMi +Private-Lines: 1 +AAAAIADMEXUw9TGuz7JykmHbzPOj8XebpZwo76iuxJtHkvAp +Private-MAC: 273204de25dcb0fc8835d6d08b8045be34358bf64d009a7566d97851449fb9ab diff --git a/test/Data/Key.PuTTY3.RSA.Encrypted.Argon2id.12345.ppk b/test/Data/Key.PuTTY3.RSA.Encrypted.Argon2id.12345.ppk new file mode 100644 index 000000000..5966f7187 --- /dev/null +++ b/test/Data/Key.PuTTY3.RSA.Encrypted.Argon2id.12345.ppk @@ -0,0 +1,31 @@ +PuTTY-User-Key-File-3: ssh-rsa +Encryption: aes256-cbc +Comment: Key.OPENSSH.RSA +Public-Lines: 6 +AAAAB3NzaC1yc2EAAAADAQABAAABAQDtbs6KCLsePWaxraXweKYs/NqBWYT8Kx4w +oJHE8xO1ZO+hl0y3uF+S2FYDuHbRruhJJ4fa3sWp46lU0YVi9FXcFVawpkkxFx0m +JMJkCMffytiT3Re9neYqso3/d9xCyHg6I+dapPodKqDXiiJXxQ+1TCcTrmyRZLG/ +G34QuVWkKobm8TY78Y0MpATsXNi3q9CKEwVIAEGqO9q7SaNfTTYpiIIyvq+CXxdi +QMDifn4nJBJDHOed+sv3dmhqq6NE/ZtPlSFeBvOvwcXC6pAa9REQJlNMjwGK//q0 +4if3HaERo3q/EMu1dz30TZ3o1bpx2uLBoYUniOBVYMTmZTTTpd09 +Key-Derivation: Argon2id +Argon2-Memory: 8192 +Argon2-Passes: 13 +Argon2-Parallelism: 1 +Argon2-Salt: 433d83be99e63cd358ef5df037448734 +Private-Lines: 14 +q3I+FoI3y6nzGIi1lIXMIx4J/yOlIYIqoHQbpQxFiUwoDxYYm8zy4BYexEw/Ox6n +LaKo4LKyvh36+5lrjqMluWsJ+PXrA+GBxSXsanzWG42RPRNbNF7RQCqTrxDRgTSv +GztaXcS5URSIutOl5vePib/9lFC5FlYRp+dCM0CVRpkUezgbZdnOQ5901DhylceT +fUXzpUcEHI4i1kp9awG+4ZpfJf057HuHFhmIvNECirt//LKBH9vrr7NQ7t1Sv8+r +NdzkcUHpCyvPLqII/zaQurgFlrYTCIyqzKHphoVZGa2XrB6VHgt/pvh+a0fOw/lB +c3t377cOr1PTht7AhYoeUPheFaYE9bPyZ5pV0Ai5IlOD94blp94BWU56e8GNx8Gp +SB0zCqrW80Q39OXb3hweyIcVEbb8dfY6sJf3PhzEL1pehUzOFSRlwjGl7BsYZCe3 +uIUyNzczpbvk6OTReGRHM7DX5xz3t/esC7VXP7CpOnjhdr9dZ6dpqGz/vDciyxV9 +Jw/gaipPAQXUf6ls3atooxN1IpLnwtPE1FHAzofV9ixW4TqHmv1H9++94VJKFyA0 +nPUVBtKhM8gT/4ZxnPEDc/sl/EYb9DmuaUC8cTV2O10Wpnj3HhS1Qh43qAguyOu/ +qF7QGETSweXTYSgresMxeLRkX/tia6tz2Rfk1HnP8sRbMyIOyOvU4pNMDuTelGy9 +7OfsS54bcBIM1DTpMZ7HiKctZ6DvjL4VtgIJPlXmc+s4CD6cjbycOHo5iQfjsoS4 +Wh2dHGK3bYVJ+LTA/Z63nasR9CLW26oL76CI/+TXvheyFotzIjtn3iKpJUqS7Dc0 +vpZT3x5OWxmYlu1xZYbzMkLIedE8WaKYjeb3qE3Ox+rN0xIYXoNiCi6jxU+viuF4 +Private-MAC: 3bf7e6fe566fab36eba63d3129929b197f6b07e234ebcf22549d10b1d97e3e2f diff --git a/test/Data/Key.PuTTY3.RSA.ppk b/test/Data/Key.PuTTY3.RSA.ppk new file mode 100644 index 000000000..c938b2dd3 --- /dev/null +++ b/test/Data/Key.PuTTY3.RSA.ppk @@ -0,0 +1,26 @@ +PuTTY-User-Key-File-3: ssh-rsa +Encryption: none +Comment: Key.OPENSSH.RSA +Public-Lines: 6 +AAAAB3NzaC1yc2EAAAADAQABAAABAQDtbs6KCLsePWaxraXweKYs/NqBWYT8Kx4w +oJHE8xO1ZO+hl0y3uF+S2FYDuHbRruhJJ4fa3sWp46lU0YVi9FXcFVawpkkxFx0m +JMJkCMffytiT3Re9neYqso3/d9xCyHg6I+dapPodKqDXiiJXxQ+1TCcTrmyRZLG/ +G34QuVWkKobm8TY78Y0MpATsXNi3q9CKEwVIAEGqO9q7SaNfTTYpiIIyvq+CXxdi +QMDifn4nJBJDHOed+sv3dmhqq6NE/ZtPlSFeBvOvwcXC6pAa9REQJlNMjwGK//q0 +4if3HaERo3q/EMu1dz30TZ3o1bpx2uLBoYUniOBVYMTmZTTTpd09 +Private-Lines: 14 +AAABAQDpeCr6CmnM632eu2zPkCN/W0eVJ6yftdpi4JFWA9veY5lK4RbcFR1NrRKv +Z+TWfNIGlSt+qc3eJ3IraDdsPWxsFEOBQpH4Bo1wI3dOnF/GDJV4mFAu8SQR2i/N +BFR/CtdF/GYTeOREZ9Vu/HKWsbynfnFyZfJ16XjqvaLx2PyAhje0qnREy9nhmU1u +FYc93k7HIdYv17eBs5LIjKNCBMpl7OHMStL9f8on9dirPIECo2pnZGDWQqIdGUdL +ooQja3IXBh+H5Fvov3FyHVKo61CFNaKubFLbl2kYPaOBqVd7KLDw+a6pOJYKpSZQ +zHox0Xe0WyKuvngrhAD2Sox5pEu1AAAAgQD+dPDqesFjwMJ9SXwWbqkLY3H5yXje +DZGEAXcm59L1buVHcqkkC2vIZQM0ToQPqib65bGYDPYfAsi08ropvJYpGR6HMDtd +8wU3VWkPHNpSb39rl0yFzWR7HkuyE5HwYjtYUgeM/EQ5Dq9+Zhn3W8iSBQMBWReF +7PFp0BfrxxGnawAAAIEA7t9vXgsFRX/YNMzR32bt9adFrRK3LEb+e36vlKD7aL/J +8VBe9aDlnuSkhpxrTCAiN9ZAbT4VG73zprqja4CQY4I2z0JotMUgBOS90LhCkTY5 +WhN/1mnSgcM4SQ7WrrmJNYn5K3QFaeu18kOabsrhoFWkATT268QPYNSG8ni+P/cA +AACBALFEE9FIau5dLoE3eGPfPWx+nltH6Jdtf5uwec5CUHqTWnVD07NfPLr7+Ip1 +vJ9jt0Qmp11h2XwidQLEfzBBFtgukA7b6ilx2831kJQmElcQdewo1ESmvHzWiAJP +fM4JjTcDudzQZXsq1IT4L5t8bewAoKc12OUcDSS/P2tFjpoM +Private-MAC: ef76b1cf66a4a28d6fe08c70012c4bfa61771502e496d227dd77580650d20bfd diff --git a/test/Renci.SshNet.Tests/Classes/PrivateKeyFileTest.cs b/test/Renci.SshNet.Tests/Classes/PrivateKeyFileTest.cs index 1e5b5f7f9..d28ba0a06 100644 --- a/test/Renci.SshNet.Tests/Classes/PrivateKeyFileTest.cs +++ b/test/Renci.SshNet.Tests/Classes/PrivateKeyFileTest.cs @@ -344,6 +344,20 @@ public void ConstructorWithFileNameAndPassPhraseShouldBeAbleToReadFileThatIsShar [DataRow("Key.OPENSSH.RSA.Encrypted.Aes.192.CTR.txt", "12345", typeof(RsaKey))] [DataRow("Key.OPENSSH.RSA.Encrypted.txt", "12345", typeof(RsaKey))] [DataRow("Key.OPENSSH.RSA.txt", null, typeof(RsaKey))] + [DataRow("Key.PuTTY2.Ed25519.Encrypted.12345.ppk", "12345", typeof(ED25519Key))] + [DataRow("Key.PuTTY2.Ed25519.ppk", null, typeof(ED25519Key))] + [DataRow("Key.PuTTY2.RSA.Encrypted.12345.ppk", "12345", typeof(RsaKey))] + [DataRow("Key.PuTTY2.RSA.ppk", null, typeof(RsaKey))] + [DataRow("Key.PuTTY3.DSA.Encrypted.Argon2id.12345.ppk", "12345", typeof(DsaKey))] + [DataRow("Key.PuTTY3.DSA.ppk", null, typeof(DsaKey))] + [DataRow("Key.PuTTY3.ECDSA.Encrypted.Argon2id.12345.ppk", "12345", typeof(EcdsaKey))] + [DataRow("Key.PuTTY3.ECDSA.ppk", null, typeof(EcdsaKey))] + [DataRow("Key.PuTTY3.Ed25519.Encrypted.Argon2i.12345.ppk", "12345", typeof(ED25519Key))] + [DataRow("Key.PuTTY3.Ed25519.Encrypted.Argon2d.12345.ppk", "12345", typeof(ED25519Key))] + [DataRow("Key.PuTTY3.Ed25519.Encrypted.Argon2id.12345.ppk", "12345", typeof(ED25519Key))] + [DataRow("Key.PuTTY3.Ed25519.ppk", null, typeof(ED25519Key))] + [DataRow("Key.PuTTY3.RSA.Encrypted.Argon2id.12345.ppk", "12345", typeof(RsaKey))] + [DataRow("Key.PuTTY3.RSA.ppk", null, typeof(RsaKey))] [DataRow("Key.RSA.Encrypted.Aes.128.CBC.12345.txt", "12345", typeof(RsaKey))] [DataRow("Key.RSA.Encrypted.Aes.192.CBC.12345.txt", "12345", typeof(RsaKey))] [DataRow("Key.RSA.Encrypted.Aes.256.CBC.12345.txt", "12345", typeof(RsaKey))] @@ -523,6 +537,86 @@ public void Test_LowercaseSalt() } } + [TestMethod] + public void PuTTYv2_InvalidMac_ThrowsSshException() + { + string pk = """ + PuTTY-User-Key-File-2: ssh-rsa + Encryption: none + Comment: Key.OPENSSH.RSA + Public-Lines: 6 + AAAAB3NzaC1yc2EAAAADAQABAAABAQDtbs6KCLsePWaxraXweKYs/NqBWYT8Kx4w + oJHE8xO1ZO+hl0y3uF+S2FYDuHbRruhJJ4fa3sWp46lU0YVi9FXcFVawpkkxFx0m + JMJkCMffytiT3Re9neYqso3/d9xCyHg6I+dapPodKqDXiiJXxQ+1TCcTrmyRZLG/ + G34QuVWkKobm8TY78Y0MpATsXNi3q9CKEwVIAEGqO9q7SaNfTTYpiIIyvq+CXxdi + QMDifn4nJBJDHOed+sv3dmhqq6NE/ZtPlSFeBvOvwcXC6pAa9REQJlNMjwGK//q0 + 4if3HaERo3q/EMu1dz30TZ3o1bpx2uLBoYUniOBVYMTmZTTTpd09 + Private-Lines: 14 + AAABAQDpeCr6CmnM632eu2zPkCN/W0eVJ6yftdpi4JFWA9veY5lK4RbcFR1NrRKv + Z+TWfNIGlSt+qc3eJ3IraDdsPWxsFEOBQpH4Bo1wI3dOnF/GDJV4mFAu8SQR2i/N + BFR/CtdF/GYTeOREZ9Vu/HKWsbynfnFyZfJ16XjqvaLx2PyAhje0qnREy9nhmU1u + FYc93k7HIdYv17eBs5LIjKNCBMpl7OHMStL9f8on9dirPIECo2pnZGDWQqIdGUdL + ooQja3IXBh+H5Fvov3FyHVKo61CFNaKubFLbl2kYPaOBqVd7KLDw+a6pOJYKpSZQ + zHox0Xe0WyKuvngrhAD2Sox5pEu1AAAAgQD+dPDqesFjwMJ9SXwWbqkLY3H5yXje + DZGEAXcm59L1buVHcqkkC2vIZQM0ToQPqib65bGYDPYfAsi08ropvJYpGR6HMDtd + 8wU3VWkPHNpSb39rl0yFzWR7HkuyE5HwYjtYUgeM/EQ5Dq9+Zhn3W8iSBQMBWReF + 7PFp0BfrxxGnawAAAIEA7t9vXgsFRX/YNMzR32bt9adFrRK3LEb+e36vlKD7aL/J + 8VBe9aDlnuSkhpxrTCAiN9ZAbT4VG73zprqja4CQY4I2z0JotMUgBOS90LhCkTY5 + WhN/1mnSgcM4SQ7WrrmJNYn5K3QFaeu18kOabsrhoFWkATT268QPYNSG8ni+P/cA + AACBALFEE9FIau5dLoE3eGPfPWx+nltH6Jdtf5uwec5CUHqTWnVD07NfPLr7+Ip1 + vJ9jt0Qmp11h2XwidQLEfzBBFtgukA7b6ilx2831kJQmElcQdewo1ESmvHzWiAJP + fM4JjTcDudzQZXsq1IT4L5t8bewAoKc12OUcDSS/P2tFjpoM + Private-MAC: 7f487d19cb5d03257c9b9a2aaaaaaaaaaaaaaaaa + """; + + using (var stream = new MemoryStream(Encoding.UTF8.GetBytes(pk))) + { + var ex = Assert.ThrowsException(() => new PrivateKeyFile(stream)); + + Assert.AreEqual("MAC verification failed for PuTTY key file", ex.Message); + } + } + + [TestMethod] + public void PuTTYv3_InvalidMac_ThrowsSshException() + { + string pk = """ + PuTTY-User-Key-File-3: ssh-rsa + Encryption: none + Comment: Key.OPENSSH.RSA + Public-Lines: 6 + AAAAB3NzaC1yc2EAAAADAQABAAABAQDtbs6KCLsePWaxraXweKYs/NqBWYT8Kx4w + oJHE8xO1ZO+hl0y3uF+S2FYDuHbRruhJJ4fa3sWp46lU0YVi9FXcFVawpkkxFx0m + JMJkCMffytiT3Re9neYqso3/d9xCyHg6I+dapPodKqDXiiJXxQ+1TCcTrmyRZLG/ + G34QuVWkKobm8TY78Y0MpATsXNi3q9CKEwVIAEGqO9q7SaNfTTYpiIIyvq+CXxdi + QMDifn4nJBJDHOed+sv3dmhqq6NE/ZtPlSFeBvOvwcXC6pAa9REQJlNMjwGK//q0 + 4if3HaERo3q/EMu1dz30TZ3o1bpx2uLBoYUniOBVYMTmZTTTpd09 + Private-Lines: 14 + AAABAQDpeCr6CmnM632eu2zPkCN/W0eVJ6yftdpi4JFWA9veY5lK4RbcFR1NrRKv + Z+TWfNIGlSt+qc3eJ3IraDdsPWxsFEOBQpH4Bo1wI3dOnF/GDJV4mFAu8SQR2i/N + BFR/CtdF/GYTeOREZ9Vu/HKWsbynfnFyZfJ16XjqvaLx2PyAhje0qnREy9nhmU1u + FYc93k7HIdYv17eBs5LIjKNCBMpl7OHMStL9f8on9dirPIECo2pnZGDWQqIdGUdL + ooQja3IXBh+H5Fvov3FyHVKo61CFNaKubFLbl2kYPaOBqVd7KLDw+a6pOJYKpSZQ + zHox0Xe0WyKuvngrhAD2Sox5pEu1AAAAgQD+dPDqesFjwMJ9SXwWbqkLY3H5yXje + DZGEAXcm59L1buVHcqkkC2vIZQM0ToQPqib65bGYDPYfAsi08ropvJYpGR6HMDtd + 8wU3VWkPHNpSb39rl0yFzWR7HkuyE5HwYjtYUgeM/EQ5Dq9+Zhn3W8iSBQMBWReF + 7PFp0BfrxxGnawAAAIEA7t9vXgsFRX/YNMzR32bt9adFrRK3LEb+e36vlKD7aL/J + 8VBe9aDlnuSkhpxrTCAiN9ZAbT4VG73zprqja4CQY4I2z0JotMUgBOS90LhCkTY5 + WhN/1mnSgcM4SQ7WrrmJNYn5K3QFaeu18kOabsrhoFWkATT268QPYNSG8ni+P/cA + AACBALFEE9FIau5dLoE3eGPfPWx+nltH6Jdtf5uwec5CUHqTWnVD07NfPLr7+Ip1 + vJ9jt0Qmp11h2XwidQLEfzBBFtgukA7b6ilx2831kJQmElcQdewo1ESmvHzWiAJP + fM4JjTcDudzQZXsq1IT4L5t8bewAoKc12OUcDSS/P2tFjpoM + Private-MAC: ef76b1cf66a4a28d6fe08c70012c4bfa61771502e496d227dddddddddddddddd + """; + + using (var stream = new MemoryStream(Encoding.UTF8.GetBytes(pk))) + { + var ex = Assert.ThrowsException(() => new PrivateKeyFile(stream)); + + Assert.AreEqual("MAC verification failed for PuTTY key file", ex.Message); + } + } + private void SaveStreamToFile(Stream stream, string fileName) { var buffer = new byte[4000]; From ee054f4d314d81e3aaf6d8ef06608146d92f33ff Mon Sep 17 00:00:00 2001 From: Varorbc Date: Wed, 4 Dec 2024 16:49:40 +0800 Subject: [PATCH 5/6] fix newline characters in PuTTY key file comment (#1550) --- src/Renci.SshNet/PrivateKeyFile.cs | 2 +- test/Data/Key.PuTTY2.RSA.Encrypted.12345.ppk | 42 ++++++++++---------- 2 files changed, 22 insertions(+), 22 deletions(-) diff --git a/src/Renci.SshNet/PrivateKeyFile.cs b/src/Renci.SshNet/PrivateKeyFile.cs index bfbe89fd1..b9dbdf5e1 100644 --- a/src/Renci.SshNet/PrivateKeyFile.cs +++ b/src/Renci.SshNet/PrivateKeyFile.cs @@ -119,7 +119,7 @@ namespace Renci.SshNet public partial class PrivateKeyFile : IPrivateKeySource, IDisposable { private const string PrivateKeyPattern = @"^-+ *BEGIN (?\w+( \w+)*) *-+\r?\n((Proc-Type: 4,ENCRYPTED\r?\nDEK-Info: (?[A-Z0-9-]+),(?[a-fA-F0-9]+)\r?\n\r?\n)|(Comment: ""?[^\r\n]*""?\r?\n))?(?([a-zA-Z0-9/+=]{1,80}\r?\n)+)(\r?\n)?-+ *END \k *-+"; - private const string PuTTYPrivateKeyPattern = @"^(?PuTTY-User-Key-File)-(?\d+): (?[\w-]+)\r?\nEncryption: (?[\w-]+)\r?\nComment: (?.*)\r?\nPublic-Lines: \d+\r?\n(?(([a-zA-Z0-9/+=]{1,64})\r?\n)+)(Key-Derivation: (?\w+)\r?\nArgon2-Memory: (?\d+)\r?\nArgon2-Passes: (?\d+)\r?\nArgon2-Parallelism: (?\d+)\r?\nArgon2-Salt: (?[a-fA-F0-9]+)\r?\n)?Private-Lines: \d+\r?\n(?(([a-zA-Z0-9/+=]{1,64})\r?\n)+)+Private-MAC: (?[a-fA-F0-9]+)"; + private const string PuTTYPrivateKeyPattern = @"^(?PuTTY-User-Key-File)-(?\d+): (?[\w-]+)\r?\nEncryption: (?[\w-]+)\r?\nComment: (?.*?)\r?\nPublic-Lines: \d+\r?\n(?(([a-zA-Z0-9/+=]{1,64})\r?\n)+)(Key-Derivation: (?\w+)\r?\nArgon2-Memory: (?\d+)\r?\nArgon2-Passes: (?\d+)\r?\nArgon2-Parallelism: (?\d+)\r?\nArgon2-Salt: (?[a-fA-F0-9]+)\r?\n)?Private-Lines: \d+\r?\n(?(([a-zA-Z0-9/+=]{1,64})\r?\n)+)+Private-MAC: (?[a-fA-F0-9]+)"; private const string CertificatePattern = @"(?[-\w]+@openssh\.com)\s(?[a-zA-Z0-9\/+=]*)(\s+(?.*))?"; #if NET7_0_OR_GREATER diff --git a/test/Data/Key.PuTTY2.RSA.Encrypted.12345.ppk b/test/Data/Key.PuTTY2.RSA.Encrypted.12345.ppk index b9c67d4db..e0a1bac22 100644 --- a/test/Data/Key.PuTTY2.RSA.Encrypted.12345.ppk +++ b/test/Data/Key.PuTTY2.RSA.Encrypted.12345.ppk @@ -2,25 +2,25 @@ PuTTY-User-Key-File-2: ssh-rsa Encryption: aes256-cbc Comment: Key.OPENSSH.RSA Public-Lines: 6 -AAAAB3NzaC1yc2EAAAADAQABAAABAQDtbs6KCLsePWaxraXweKYs/NqBWYT8Kx4w -oJHE8xO1ZO+hl0y3uF+S2FYDuHbRruhJJ4fa3sWp46lU0YVi9FXcFVawpkkxFx0m -JMJkCMffytiT3Re9neYqso3/d9xCyHg6I+dapPodKqDXiiJXxQ+1TCcTrmyRZLG/ -G34QuVWkKobm8TY78Y0MpATsXNi3q9CKEwVIAEGqO9q7SaNfTTYpiIIyvq+CXxdi -QMDifn4nJBJDHOed+sv3dmhqq6NE/ZtPlSFeBvOvwcXC6pAa9REQJlNMjwGK//q0 -4if3HaERo3q/EMu1dz30TZ3o1bpx2uLBoYUniOBVYMTmZTTTpd09 +AAAAB3NzaC1yc2EAAAABJQAAAQEAhV8n1KDNo6N3dkNbIf8PhKNxxb68Sgy9abub +6Q17ozTKKFOVyKTy2Du3ZJ03ep8J3/4OzCbmH88KKcp3MtdWCX6DhEB8Tt7MMcL4 +7KgLQoHshe+DH09XmdYBmPNCdT6mqOB3UaoEzHDUspP/dAsIaOAlu31gqXhDb4I3 +SL5X5N3cACm+C33aKlL21XZTB+WJtFvl6DslkbLOaELeji25ZB5ALU6+iyUvMEqQ +nWHRkpZENaoaP3Na6q5OJKEGKd+ur5MuehlxMvgvNIfi431IHZmY9IrxMDryztYd +BctztWzHS8Fo3REj5jvIXzxK9hK5txFmi3TCaI5js/XdYwZr7Q== Private-Lines: 14 -i38C+M7U/qOvzvOdtEC60v3cn354TMdDpAVk22UuNgQY7u6avW907mUuXjqU7Tjy -G66uQ+6vOnaQR6xTYWn3uI1YMWGsRylLiNZ/O/dIpITv+9xuU5U8qB+nFIH3iD6U -cBuumRKH2k2IxYVBO1nKTt3T3HejIIy3JlFifs9ylahgPD1m8jIfNARnYB09fOQr -g+nV4YNRntOqpf+cUpc+TGnz35oa9O+6fzJY/2hdkOJ+fUBdlYmcKEZZ9PgYHAlM -+aoQhL1vRoqZqzfHZYcMUBlmbRMgWiPfFB59nU0QbZ2uClbFxGGehBnCAsnd8Nni -TafVYckp3kCNLmctrmorsRgM/IXWcdqmrGJO5sHEsutQqs9Dp3KM9xhRP23xoGnZ -ssNw1qEAOP/k9JTCID/oeRE7RvAEMEdCs3l6FHKDvInR5xDuNrGehHdc3vGqklkC -1kBER2vWRX+LANNXcfISRDNajC6XKsDo6aGJRPTMj/tbVJNbjP4Oi6fwcFizV9Z0 -yJiw1yV5AjUc8nG3cuTb1wNZ8DTPccq+W6ir8U+Vmdc67Mhxas6e2UKa5nXC+HqC -jtM1oCPpxNyRoJSuxcWrwcs/yEhFfSOBU9PFsbJcX5i+ilZ2ZSipFc2fOcSx/6RC -U36mELhmHPINoTDOc6qvL/fXRkqtoQGZm2UHoINVLkeMu3ERkxxDk9kFqRcSdFd+ -nSMJt9d3YRSsjhzvro65ugKiYBubugon24frB3nARI9zoe0BZB9H/RW3RPlMT8d9 -H+gElqyiYWZKKJs+Qht6NY0Hv1gp9erMIcTTts/w7ZRc5oS8pLCKOuB9wKYepJm7 -5I0x8qVotiHglDJ7PPbP+3UzrJvGp2nwPzj4qokCZKFhPYNYiCY16V0LaBRoE3mQ -Private-MAC: 191817999553b67a7516223647e90e0b17db8918 +7aKnXgEFpI13WMiMB/Fxh4TZU56/tVDjrSFZOacypsS8DMdw+ovbblDRD9tzv0XO +1tRuV0K/IIkHu7PrszeWzq3MS1QMmOyOYePZtzIfEyAOBSjR4humXZKw7JZE313u +Lea/o4PgaDJAVtWTsF42o+0bNmYL3NIUNawTb+t61hVk8cEwJm2evo/ZzLE9uCeR +X6n8MJ41p/6803oCg6o7V2Qwk9Zadv6UzQDv1fLB18ZjD40FfyEWDe1jWbIZ0H1r +4XS8MIdAVXwkhqMhwzzXPtT9CdEFHY5tR0L3mKq7TrcRwJTuUbct5uHe7HeoeNWN +NjCxj9MazAMaqGe43q/FwPPXbYY7OvImFcUWFJO/k9ysVqxg5HlgLBvsl9gtXGRz +kaQgHP+2iM9GjxlJcCbviE7yD92zbfbR5moIeINqFa7TMlXGWCteJ00qyh3RbUVb +D1yJnBzKJ7S7Y3a4T861d4d5OHVhPVBpcGB43FZC0h6qChqZ0eZtRx4J6e+f/32n +6g4yvja/0nGUkrHvutfOYV0hTShGYXrIKxgBu7NYxgHdQnUnxq0e2kF9xtskXZVb +pk6ZaDbNHQW7AvUebJFnoPCPjvF8UVFJ2jimsro4PECViH3Sj9PoFpLMBmWVomtz ++EXkrISVIdudQVZPXtDp3/oRuW5Zfj5hfrxG9lsuxxtXJoBBXVVhxOVpCAzyw6lI +KN5ZdDry7GuPMDp3JBdqtAOrz2p9Z2XJp3NCgwuNjPu3qGngH/DGHi1ZA6rhzyHk +3tT96ww6Lmgiza1/d+DGNgYiRWB0aF5+Gj4ixb6jIv/OhCxHPL8dEN1HxxwanrUp +vGbhRv5xDWdf7eYuGyCUnkrlNjC7jvEOQNwx6KxLGzLcSdbIRmwmtjbbWNFk6qJv +Private-MAC: 63bf8fb4565012276cdf442031308b2157b79f67 From 021ee997b6ba421a8bc60d740d72b471818ca594 Mon Sep 17 00:00:00 2001 From: Rob Hague Date: Wed, 18 Dec 2024 17:15:51 +0000 Subject: [PATCH 6/6] Pin Alpine Linux image to 3.20 (#1554) --- .../{Dockerfile.TestServer => Dockerfile} | 2 +- .../TestsFixtures/InfrastructureFixture.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) rename test/Renci.SshNet.IntegrationTests/{Dockerfile.TestServer => Dockerfile} (99%) diff --git a/test/Renci.SshNet.IntegrationTests/Dockerfile.TestServer b/test/Renci.SshNet.IntegrationTests/Dockerfile similarity index 99% rename from test/Renci.SshNet.IntegrationTests/Dockerfile.TestServer rename to test/Renci.SshNet.IntegrationTests/Dockerfile index 548f76dab..d2ef74223 100644 --- a/test/Renci.SshNet.IntegrationTests/Dockerfile.TestServer +++ b/test/Renci.SshNet.IntegrationTests/Dockerfile @@ -1,4 +1,4 @@ -FROM alpine:latest +FROM alpine:3.20 COPY --chown=root:root server/ssh /etc/ssh/ COPY --chown=root:root server/script /opt/sshnet diff --git a/test/Renci.SshNet.IntegrationTests/TestsFixtures/InfrastructureFixture.cs b/test/Renci.SshNet.IntegrationTests/TestsFixtures/InfrastructureFixture.cs index 7c095b867..93d44e015 100644 --- a/test/Renci.SshNet.IntegrationTests/TestsFixtures/InfrastructureFixture.cs +++ b/test/Renci.SshNet.IntegrationTests/TestsFixtures/InfrastructureFixture.cs @@ -43,7 +43,7 @@ public async Task InitializeAsync() _sshServerImage = new ImageFromDockerfileBuilder() .WithName("renci-ssh-tests-server-image") .WithDockerfileDirectory(CommonDirectoryPath.GetSolutionDirectory(), Path.Combine("test", "Renci.SshNet.IntegrationTests")) - .WithDockerfile("Dockerfile.TestServer") + .WithDockerfile("Dockerfile") .WithDeleteIfExists(true) .WithLogger(containerLogger) .Build();