diff --git a/Directory.Packages.props b/Directory.Packages.props index 432be66..a6d52bc 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -38,8 +38,8 @@ - - + + diff --git a/OpenSSH_GUI.Tests/Core/Lib/Keys/PuttyKeyFileCompatibilityTests.cs b/OpenSSH_GUI.Tests/Core/Lib/Keys/PuttyKeyFileCompatibilityTests.cs new file mode 100644 index 0000000..f182f82 --- /dev/null +++ b/OpenSSH_GUI.Tests/Core/Lib/Keys/PuttyKeyFileCompatibilityTests.cs @@ -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; + +/// +/// 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 , which is how the app opens .ppk files. +/// +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]; +}