122 lines
4.2 KiB
C#
122 lines
4.2 KiB
C#
using ServerMonitorManager_Desktop;
|
|
using Xunit;
|
|
|
|
namespace ServerMonitorManager.Desktop.Security.Tests;
|
|
|
|
public sealed class SshHostKeyTrustTests : IDisposable
|
|
{
|
|
private readonly string _directory = Path.Combine(
|
|
Path.GetTempPath(),
|
|
$"smm-host-key-tests-{Guid.NewGuid():N}");
|
|
|
|
[Fact]
|
|
public void ParseCandidatePrefersEd25519AndComputesSha256Fingerprint()
|
|
{
|
|
var candidate = SshHostKeyTrust.ParseCandidate(
|
|
"server.example",
|
|
22,
|
|
"server.example ssh-rsa AQIDBA==\nserver.example ssh-ed25519 AQIDBA==\n");
|
|
|
|
Assert.Equal("ssh-ed25519", candidate.KeyType);
|
|
Assert.Equal("SHA256:n2SnR+G5fxMfq7a0Rylsm28CAeefs8U1bmx36JtqgGo", candidate.Fingerprint);
|
|
Assert.Equal("server.example ssh-ed25519 AQIDBA==", candidate.KnownHostsLine);
|
|
}
|
|
|
|
[Fact]
|
|
public void ParseCandidateRejectsUnexpectedEndpoint()
|
|
{
|
|
var exception = Assert.Throws<InvalidOperationException>(() =>
|
|
SshHostKeyTrust.ParseCandidate(
|
|
"server.example",
|
|
2222,
|
|
"[other.example]:2222 ssh-ed25519 AQIDBA==\n"));
|
|
|
|
Assert.Contains("endpoint", exception.Message, StringComparison.OrdinalIgnoreCase);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task WriteAsyncReplacesLegacyPatternsWithOneExclusivePin()
|
|
{
|
|
Directory.CreateDirectory(_directory);
|
|
var path = Path.Combine(_directory, "known_hosts");
|
|
await File.WriteAllTextAsync(
|
|
path,
|
|
"|1|legacy-salt|legacy-hash ssh-rsa BQYHCA==\n@cert-authority *.example ssh-ed25519 BQYHCA==\n[server.example]:2222\tssh-rsa BQYHCA==\n",
|
|
TestContext.Current.CancellationToken);
|
|
var candidate = SshHostKeyTrust.ParseCandidate(
|
|
"server.example",
|
|
2222,
|
|
"[server.example]:2222 ssh-ed25519 AQIDBA==\n");
|
|
|
|
await SshHostKeyTrust.WriteAsync(
|
|
path,
|
|
candidate,
|
|
TestContext.Current.CancellationToken);
|
|
|
|
var lines = await File.ReadAllLinesAsync(path, TestContext.Current.CancellationToken);
|
|
Assert.Equal([candidate.KnownHostsLine], lines);
|
|
Assert.True(SshHostKeyTrust.IsTrusted(
|
|
path,
|
|
"server.example",
|
|
2222,
|
|
candidate.Fingerprint));
|
|
Assert.False(SshHostKeyTrust.IsTrusted(
|
|
path,
|
|
"server.example",
|
|
2222,
|
|
"SHA256:wrong"));
|
|
}
|
|
|
|
[Fact]
|
|
public async Task IsTrustedRejectsAnAdditionalEndpointKey()
|
|
{
|
|
Directory.CreateDirectory(_directory);
|
|
var path = Path.Combine(_directory, "known_hosts");
|
|
await File.WriteAllTextAsync(
|
|
path,
|
|
"server.example ssh-ed25519 AQIDBA==\nserver.example ssh-rsa BQYHCA==\n",
|
|
TestContext.Current.CancellationToken);
|
|
|
|
Assert.False(SshHostKeyTrust.IsTrusted(
|
|
path,
|
|
"server.example",
|
|
22,
|
|
"SHA256:n2SnR+G5fxMfq7a0Rylsm28CAeefs8U1bmx36JtqgGo"));
|
|
}
|
|
|
|
[Fact]
|
|
public async Task IsTrustedRejectsAnAdditionalCertificateAuthority()
|
|
{
|
|
Directory.CreateDirectory(_directory);
|
|
var path = Path.Combine(_directory, "known_hosts");
|
|
await File.WriteAllTextAsync(
|
|
path,
|
|
"server.example ssh-ed25519 AQIDBA==\n@cert-authority server.example ssh-ed25519 BQYHCA==\n",
|
|
TestContext.Current.CancellationToken);
|
|
|
|
Assert.False(SshHostKeyTrust.IsTrusted(
|
|
path,
|
|
"server.example",
|
|
22,
|
|
"SHA256:n2SnR+G5fxMfq7a0Rylsm28CAeefs8U1bmx36JtqgGo"));
|
|
}
|
|
|
|
[Fact]
|
|
public void GetPinPathIsEndpointScopedAndDoesNotExposeHostname()
|
|
{
|
|
var first = SshHostKeyTrust.GetPinPath(_directory, "server.example", 22);
|
|
var second = SshHostKeyTrust.GetPinPath(_directory, "server.example", 2222);
|
|
|
|
Assert.NotEqual(first, second);
|
|
Assert.Equal(_directory, Path.GetDirectoryName(first));
|
|
Assert.DoesNotContain("server.example", Path.GetFileName(first), StringComparison.OrdinalIgnoreCase);
|
|
}
|
|
|
|
public void Dispose()
|
|
{
|
|
if (Directory.Exists(_directory))
|
|
{
|
|
Directory.Delete(_directory, recursive: true);
|
|
}
|
|
}
|
|
}
|