Fix SSH fingerprint checking

This commit is contained in:
nicolas.dorier 2018-08-13 09:43:59 +09:00
parent 322518e9dc
commit 214b2d1c1c
9 changed files with 204 additions and 104 deletions

View File

@ -849,6 +849,22 @@ namespace BTCPayServer.Tests
Assert.Equal("abed2", search.Filters["status"].Skip(1).First());
}
[Fact]
public void CanParseFingerprint()
{
Assert.True(SSH.SSHFingerprint.TryParse("4e343c6fc6cfbf9339c02d06a151e1dd", out var unused));
Assert.Equal("4e:34:3c:6f:c6:cf:bf:93:39:c0:2d:06:a1:51:e1:dd", unused.ToString());
Assert.True(SSH.SSHFingerprint.TryParse("4e:34:3c:6f:c6:cf:bf:93:39:c0:2d:06:a1:51:e1:dd", out unused));
Assert.True(SSH.SSHFingerprint.TryParse("SHA256:Wl7CdRgT4u5T7yPMsxSrlFP+HIJJWwidGkzphJ8di5w", out unused));
Assert.True(SSH.SSHFingerprint.TryParse("SHA256:Wl7CdRgT4u5T7yPMsxSrlFP+HIJJWwidGkzphJ8di5w=", out unused));
Assert.True(SSH.SSHFingerprint.TryParse("Wl7CdRgT4u5T7yPMsxSrlFP+HIJJWwidGkzphJ8di5w=", out unused));
Assert.Equal("SHA256:Wl7CdRgT4u5T7yPMsxSrlFP+HIJJWwidGkzphJ8di5w", unused.ToString());
Assert.True(SSH.SSHFingerprint.TryParse("Wl7CdRgT4u5T7yPMsxSrlFP+HIJJWwidGkzphJ8di5w=", out var f1));
Assert.True(SSH.SSHFingerprint.TryParse("SHA256:Wl7CdRgT4u5T7yPMsxSrlFP+HIJJWwidGkzphJ8di5w", out var f2));
Assert.Equal(f1.ToString(), f2.ToString());
}
[Fact]
public void TestAccessBitpayAPI()
{

View File

@ -2,7 +2,7 @@
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>netcoreapp2.1</TargetFramework>
<Version>1.0.2.89</Version>
<Version>1.0.2.90</Version>
<NoWarn>NU1701,CA1816,CA1308,CA1810,CA2208</NoWarn>
</PropertyGroup>
<ItemGroup>

View File

@ -13,6 +13,7 @@ using NBXplorer;
using BTCPayServer.Payments.Lightning;
using Renci.SshNet;
using NBitcoin.DataEncoders;
using BTCPayServer.SSH;
namespace BTCPayServer.Configuration
{
@ -23,69 +24,6 @@ namespace BTCPayServer.Configuration
public string CookieFile { get; internal set; }
}
public class SSHSettings
{
public string Server { get; set; }
public int Port { get; set; } = 22;
public string KeyFile { get; set; }
public string KeyFilePassword { get; set; }
public string Username { get; set; }
public string Password { get; set; }
public ConnectionInfo CreateConnectionInfo()
{
if (!string.IsNullOrEmpty(KeyFile))
{
return new ConnectionInfo(Server, Port, Username, new[] { new PrivateKeyAuthenticationMethod(Username, new PrivateKeyFile(KeyFile, KeyFilePassword)) });
}
else
{
return new ConnectionInfo(Server, Port, Username, new[] { new PasswordAuthenticationMethod(Username, Password) });
}
}
public static SSHSettings ParseConfiguration(IConfiguration conf)
{
var externalUrl = conf.GetOrDefault<Uri>("externalurl", null);
var settings = new SSHSettings();
settings.Server = conf.GetOrDefault<string>("sshconnection", null);
if (settings.Server != null)
{
var parts = settings.Server.Split(':');
if (parts.Length == 2 && int.TryParse(parts[1], out int port))
{
settings.Port = port;
settings.Server = parts[0];
}
else
{
settings.Port = 22;
}
parts = settings.Server.Split('@');
if (parts.Length == 2)
{
settings.Username = parts[0];
settings.Server = parts[1];
}
else
{
settings.Username = "root";
}
}
else if (externalUrl != null)
{
settings.Port = 22;
settings.Username = "root";
settings.Server = externalUrl.DnsSafeHost;
}
settings.Password = conf.GetOrDefault<string>("sshpassword", "");
settings.KeyFile = conf.GetOrDefault<string>("sshkeyfile", "");
settings.KeyFilePassword = conf.GetOrDefault<string>("sshkeyfilepassword", "");
return settings;
}
}
public class BTCPayServerOptions
{
public NetworkType NetworkType
@ -182,11 +120,18 @@ namespace BTCPayServer.Configuration
BundleJsCss = conf.GetOrDefault<bool>("bundlejscss", true);
ExternalUrl = conf.GetOrDefault<Uri>("externalurl", null);
var sshSettings = SSHSettings.ParseConfiguration(conf);
var sshSettings = ParseSSHConfiguration(conf);
if ((!string.IsNullOrEmpty(sshSettings.Password) || !string.IsNullOrEmpty(sshSettings.KeyFile)) && !string.IsNullOrEmpty(sshSettings.Server))
{
if (!string.IsNullOrEmpty(sshSettings.KeyFile) && !File.Exists(sshSettings.KeyFile))
throw new ConfigException($"sshkeyfile does not exist");
int waitTime = 0;
while (!string.IsNullOrEmpty(sshSettings.KeyFile) && !File.Exists(sshSettings.KeyFile))
{
if(waitTime++ < 5)
System.Threading.Thread.Sleep(1000);
else
throw new ConfigException($"sshkeyfile does not exist");
}
if (sshSettings.Port > ushort.MaxValue ||
sshSettings.Port < ushort.MinValue)
throw new ConfigException($"ssh port is invalid");
@ -206,10 +151,11 @@ namespace BTCPayServer.Configuration
var fingerPrints = conf.GetOrDefault<string>("sshtrustedfingerprints", "");
if (!string.IsNullOrEmpty(fingerPrints))
{
foreach (var fingerprint in fingerPrints.Split(';', StringSplitOptions.RemoveEmptyEntries)
.Select(str => str.Replace(":", "", StringComparison.OrdinalIgnoreCase)))
foreach (var fingerprint in fingerPrints.Split(';', StringSplitOptions.RemoveEmptyEntries))
{
TrustedFingerprints.Add(DecodeFingerprint(fingerprint));
if (!SSHFingerprint.TryParse(fingerprint, out var f))
throw new ConfigException($"Invalid ssh fingerprint format {fingerprint}");
TrustedFingerprints.Add(f);
}
}
@ -221,42 +167,50 @@ namespace BTCPayServer.Configuration
throw new ConfigException($"internallightningnode should not be used anymore, use btclightning instead");
}
private static byte[] DecodeFingerprint(string fingerprint)
private SSHSettings ParseSSHConfiguration(IConfiguration conf)
{
try
var externalUrl = conf.GetOrDefault<Uri>("externalurl", null);
var settings = new SSHSettings();
settings.Server = conf.GetOrDefault<string>("sshconnection", null);
if (settings.Server != null)
{
return Encoders.Hex.DecodeData(fingerprint.Trim());
}
catch
{
}
var parts = settings.Server.Split(':');
if (parts.Length == 2 && int.TryParse(parts[1], out int port))
{
settings.Port = port;
settings.Server = parts[0];
}
else
{
settings.Port = 22;
}
var localFingerprint = fingerprint;
if (localFingerprint.StartsWith("SHA256", StringComparison.OrdinalIgnoreCase))
localFingerprint = localFingerprint.Substring("SHA256".Length).Trim();
try
{
return Encoders.Base64.DecodeData(localFingerprint);
parts = settings.Server.Split('@');
if (parts.Length == 2)
{
settings.Username = parts[0];
settings.Server = parts[1];
}
else
{
settings.Username = "root";
}
}
catch
else if (externalUrl != null)
{
settings.Port = 22;
settings.Username = "root";
settings.Server = externalUrl.DnsSafeHost;
}
if (!localFingerprint.EndsWith('='))
localFingerprint = localFingerprint + "=";
try
{
return Encoders.Base64.DecodeData(localFingerprint);
}
catch
{
throw new ConfigException($"sshtrustedfingerprints is invalid");
}
settings.Password = conf.GetOrDefault<string>("sshpassword", "");
settings.KeyFile = conf.GetOrDefault<string>("sshkeyfile", "");
settings.KeyFilePassword = conf.GetOrDefault<string>("sshkeyfilepassword", "");
return settings;
}
internal bool IsTrustedFingerprint(byte[] fingerPrint)
internal bool IsTrustedFingerprint(byte[] fingerPrint, byte[] hostKey)
{
return TrustedFingerprints.Any(f => Utils.ArrayEqual(f, fingerPrint));
return TrustedFingerprints.Any(f => f.Match(fingerPrint, hostKey));
}
public string RootPath { get; set; }
@ -279,7 +233,7 @@ namespace BTCPayServer.Configuration
get;
set;
}
public List<byte[]> TrustedFingerprints { get; set; } = new List<byte[]>();
public List<SSHFingerprint> TrustedFingerprints { get; set; } = new List<SSHFingerprint>();
public SSHSettings SSHSettings
{
get;

View File

@ -38,7 +38,7 @@ namespace BTCPayServer.Configuration
app.Option("--sshpassword", "SSH password to manage BTCPay (default: empty)", CommandOptionType.SingleValue);
app.Option("--sshkeyfile", "SSH private key file to manage BTCPay (default: empty)", CommandOptionType.SingleValue);
app.Option("--sshkeyfilepassword", "Password of the SSH keyfile (default: empty)", CommandOptionType.SingleValue);
app.Option("--sshtrustedfingerprints", "SSH Host SHA256 rsa fingerprint in base64 or hex (default: empty, it will allow untrusted connections)", CommandOptionType.SingleValue);
app.Option("--sshtrustedfingerprints", "SSH Host public key fingerprint or sha256 (default: empty, it will allow untrusted connections)", CommandOptionType.SingleValue);
foreach (var network in provider.GetAll())
{
var crypto = network.CryptoCode.ToLowerInvariant();

View File

@ -267,14 +267,14 @@ namespace BTCPayServer.Controllers
{
if (_Options.TrustedFingerprints.Count == 0)
{
Logs.Configuration.LogWarning($"SSH host fingerprint for {e.HostKey} is untrusted, start BTCPay with -sshtrustedfingerprints \"{Encoders.Hex.EncodeData(e.FingerPrint)}\"");
Logs.Configuration.LogWarning($"SSH host fingerprint for {e.HostKeyName} is untrusted, start BTCPay with -sshtrustedfingerprints \"{Encoders.Hex.EncodeData(e.FingerPrint)}\"");
e.CanTrust = true; // Not a typo, we want the connection to succeed with a warning
}
else
{
e.CanTrust = _Options.IsTrustedFingerprint(e.FingerPrint);
e.CanTrust = _Options.IsTrustedFingerprint(e.FingerPrint, e.HostKey);
if(!e.CanTrust)
Logs.Configuration.LogError($"SSH host fingerprint for {e.HostKey} is untrusted, start BTCPay with -sshtrustedfingerprints \"{Encoders.Hex.EncodeData(e.FingerPrint)}\"");
Logs.Configuration.LogError($"SSH host fingerprint for {e.HostKeyName} is untrusted, start BTCPay with -sshtrustedfingerprints \"{Encoders.Hex.EncodeData(e.FingerPrint)}\"");
}
};
}

View File

@ -34,9 +34,9 @@ namespace BTCPayServer.HostedServices
connection.HostKeyReceived += (object sender, Renci.SshNet.Common.HostKeyEventArgs e) =>
{
e.CanTrust = true;
if (!_options.IsTrustedFingerprint(e.FingerPrint))
if (!_options.IsTrustedFingerprint(e.FingerPrint, e.HostKey))
{
Logs.Configuration.LogWarning($"SSH host fingerprint for {e.HostKey} is untrusted, start BTCPay with -sshtrustedfingerprints \"{Encoders.Hex.EncodeData(e.FingerPrint)}\"");
Logs.Configuration.LogWarning($"SSH host fingerprint for {e.HostKeyName} is untrusted, start BTCPay with -sshtrustedfingerprints \"{Encoders.Hex.EncodeData(e.FingerPrint)}\"");
}
};
try

View File

@ -4,6 +4,7 @@ using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Threading.Tasks;
using BTCPayServer.Configuration;
using BTCPayServer.SSH;
using Renci.SshNet;
namespace BTCPayServer.Models.ServerViewModels

View File

@ -0,0 +1,98 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using NBitcoin;
using NBitcoin.DataEncoders;
namespace BTCPayServer.SSH
{
public class SSHFingerprint
{
public static bool TryParse(string str, out SSHFingerprint fingerPrint)
{
if (str == null)
throw new ArgumentNullException(nameof(str));
fingerPrint = null;
str = str.Trim();
try
{
var shortFingerprint = str.Replace(":", "", StringComparison.OrdinalIgnoreCase);
if (HexEncoder.IsWellFormed(shortFingerprint))
{
var hash = Encoders.Hex.DecodeData(shortFingerprint);
if (hash.Length == 16)
{
fingerPrint = new SSHFingerprint(hash);
return true;
}
return false;
}
}
catch
{
}
if (str.StartsWith("SHA256:", StringComparison.OrdinalIgnoreCase))
str = str.Substring("SHA256:".Length).Trim();
if (str.Contains(':', StringComparison.OrdinalIgnoreCase))
return false;
if (!str.EndsWith('='))
str = str + "=";
try
{
var hash = Encoders.Base64.DecodeData(str);
if (hash.Length == 32)
{
fingerPrint = new SSHFingerprint(hash);
return true;
}
}
catch
{
}
return false;
}
public SSHFingerprint(byte[] hash)
{
if (hash.Length == 16)
{
_ShortFingerprint = hash;
_Original = string.Join(':', hash.Select(b => b.ToString("x2", CultureInfo.InvariantCulture))
.ToArray());
}
else if (hash.Length == 32)
{
_FullHash = hash;
_Original = "SHA256:" + Encoders.Base64.EncodeData(hash);
if (_Original.EndsWith("=", StringComparison.OrdinalIgnoreCase))
_Original = _Original.Substring(0, _Original.Length - 1);
}
else
throw new ArgumentException(paramName:nameof(hash), message: "Invalid length, expected 16 or 32");
}
byte[] _ShortFingerprint;
byte[] _FullHash;
public bool Match(byte[] shortFingerprint, byte[] hostKey)
{
if (shortFingerprint == null)
throw new ArgumentNullException(nameof(shortFingerprint));
if (hostKey == null)
throw new ArgumentNullException(nameof(hostKey));
if (_ShortFingerprint != null)
return Utils.ArrayEqual(shortFingerprint, _ShortFingerprint);
return Utils.ArrayEqual(_FullHash, NBitcoin.Crypto.Hashes.SHA256(hostKey));
}
string _Original;
public override string ToString()
{
return _Original;
}
}
}

View File

@ -0,0 +1,31 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.Extensions.Configuration;
using Renci.SshNet;
namespace BTCPayServer.SSH
{
public class SSHSettings
{
public string Server { get; set; }
public int Port { get; set; } = 22;
public string KeyFile { get; set; }
public string KeyFilePassword { get; set; }
public string Username { get; set; }
public string Password { get; set; }
public ConnectionInfo CreateConnectionInfo()
{
if (!string.IsNullOrEmpty(KeyFile))
{
return new ConnectionInfo(Server, Port, Username, new[] { new PrivateKeyAuthenticationMethod(Username, new PrivateKeyFile(KeyFile, KeyFilePassword)) });
}
else
{
return new ConnectionInfo(Server, Port, Username, new[] { new PasswordAuthenticationMethod(Username, Password) });
}
}
}
}