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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,8 @@
<PackageVersion Include="Serilog.Sinks.Console" Version="6.1.1" />
<PackageVersion Include="Serilog.Sinks.File" Version="7.0.0" />
<PackageVersion Include="Shouldly" Version="4.3.0" />
<PackageVersion Include="SSH.NET" Version="2025.1.0" />
<PackageVersion Include="SshNet.Keygen" Version="2024.2.0.2" />
<PackageVersion Include="SSH.NET" Version="2026.0.0" />
<PackageVersion Include="SshNet.Keygen" Version="2026.0.0" />
<PackageVersion Include="SshNet.PuttyKeyFile" Version="2024.2.0.3" />
<PackageVersion Include="Svg.Controls.Skia.Avalonia" Version="12.0.0.13" />
<PackageVersion Include="System.Interactive" Version="7.0.1" />
Expand Down
46 changes: 46 additions & 0 deletions OpenSSH_GUI.Tests/Core/Lib/Keys/PuttyKeyFileCompatibilityTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
using Renci.SshNet;
using Shouldly;
using SshNet.Keygen;
using SshNet.Keygen.Extensions;
using SshNet.Keygen.SshKeyEncryption;
using Xunit;

namespace OpenSSH_GUI.Tests.Core.Lib.Keys;

/// <summary>
/// Guards the PPK round trip across SSH.NET and SshNet.Keygen upgrades: keys written by SshNet.Keygen in PuTTY format
/// must load with SSH.NET's <see cref="PrivateKeyFile" />, which is how the app opens <c>.ppk</c> files.
/// </summary>
public sealed class PuttyKeyFileCompatibilityTests : IDisposable
{
private readonly string directory = Directory.CreateTempSubdirectory("putty-compat").FullName;

public void Dispose() { Directory.Delete(directory, true); }

[Theory]
[InlineData(SshKeyType.RSA, SshKeyFormat.PuTTYv2, null)]
[InlineData(SshKeyType.RSA, SshKeyFormat.PuTTYv3, null)]
[InlineData(SshKeyType.ED25519, SshKeyFormat.PuTTYv2, null)]
[InlineData(SshKeyType.ED25519, SshKeyFormat.PuTTYv3, null)]
[InlineData(SshKeyType.ECDSA, SshKeyFormat.PuTTYv3, null)]
[InlineData(SshKeyType.RSA, SshKeyFormat.PuTTYv2, "ppk-passphrase")]
[InlineData(SshKeyType.ED25519, SshKeyFormat.PuTTYv3, "ppk-passphrase")]
[InlineData(SshKeyType.ECDSA, SshKeyFormat.PuTTYv3, "ppk-passphrase")]
public void PrivateKeyFile_PpkWrittenBySshNetKeygen_LoadsSameKey(SshKeyType keyType, SshKeyFormat format, string? passphrase)
{
var path = Path.Combine(directory, $"id_{keyType}_{format}.ppk");
var info = new SshKeyGenerateInfo(keyType)
{
Comment = "putty-compat",
KeyFormat = format
};
if (passphrase is not null) info.Encryption = new SshKeyEncryptionAes256(passphrase);
var generated = SshKey.Generate(path, FileMode.CreateNew, info);

using var loaded = passphrase is null ? new PrivateKeyFile(path) : new PrivateKeyFile(path, passphrase);

GetPublicKeyBlob(loaded).ShouldBe(GetPublicKeyBlob(generated));
}

private static string GetPublicKeyBlob(IPrivateKeySource keySource) => keySource.ToOpenSshPublicFormat().Split(' ')[1];
}
Loading