diff --git a/BTCPayServer/BTCPayServer.csproj b/BTCPayServer/BTCPayServer.csproj index ef9406d0d..99c4d0705 100644 --- a/BTCPayServer/BTCPayServer.csproj +++ b/BTCPayServer/BTCPayServer.csproj @@ -2,7 +2,7 @@ Exe netcoreapp2.1 - 1.0.2.88 + 1.0.2.89 NU1701,CA1816,CA1308,CA1810,CA2208 diff --git a/BTCPayServer/Configuration/BTCPayServerOptions.cs b/BTCPayServer/Configuration/BTCPayServerOptions.cs index 0b45c937a..2587e5fae 100644 --- a/BTCPayServer/Configuration/BTCPayServerOptions.cs +++ b/BTCPayServer/Configuration/BTCPayServerOptions.cs @@ -12,6 +12,7 @@ using Microsoft.Extensions.Configuration; using NBXplorer; using BTCPayServer.Payments.Lightning; using Renci.SshNet; +using NBitcoin.DataEncoders; namespace BTCPayServer.Configuration { @@ -72,7 +73,7 @@ namespace BTCPayServer.Configuration settings.Username = "root"; } } - else if(externalUrl != null) + else if (externalUrl != null) { settings.Port = 22; settings.Username = "root"; @@ -182,7 +183,7 @@ namespace BTCPayServer.Configuration ExternalUrl = conf.GetOrDefault("externalurl", null); var sshSettings = SSHSettings.ParseConfiguration(conf); - if (!string.IsNullOrEmpty(sshSettings.Password) || !string.IsNullOrEmpty(sshSettings.KeyFile)) + 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"); @@ -199,10 +200,19 @@ namespace BTCPayServer.Configuration { throw new ConfigException($"sshkeyfilepassword is invalid"); } - SSHSettings = sshSettings; } + var fingerPrints = conf.GetOrDefault("sshtrustedfingerprints", ""); + if (!string.IsNullOrEmpty(fingerPrints)) + { + foreach (var fingerprint in fingerPrints.Split(';', StringSplitOptions.RemoveEmptyEntries) + .Select(str => str.Replace(":", "", StringComparison.OrdinalIgnoreCase))) + { + TrustedFingerprints.Add(DecodeFingerprint(fingerprint)); + } + } + RootPath = conf.GetOrDefault("rootpath", "/"); if (!RootPath.StartsWith("/", StringComparison.InvariantCultureIgnoreCase)) RootPath = "/" + RootPath; @@ -210,6 +220,45 @@ namespace BTCPayServer.Configuration if (old != null) throw new ConfigException($"internallightningnode should not be used anymore, use btclightning instead"); } + + private static byte[] DecodeFingerprint(string fingerprint) + { + try + { + return Encoders.Hex.DecodeData(fingerprint.Trim()); + } + catch + { + } + + var localFingerprint = fingerprint; + if (localFingerprint.StartsWith("SHA256", StringComparison.OrdinalIgnoreCase)) + localFingerprint = localFingerprint.Substring("SHA256".Length).Trim(); + try + { + return Encoders.Base64.DecodeData(localFingerprint); + } + catch + { + } + + if (!localFingerprint.EndsWith('=')) + localFingerprint = localFingerprint + "="; + try + { + return Encoders.Base64.DecodeData(localFingerprint); + } + catch + { + throw new ConfigException($"sshtrustedfingerprints is invalid"); + } + } + + internal bool IsTrustedFingerprint(byte[] fingerPrint) + { + return TrustedFingerprints.Any(f => Utils.ArrayEqual(f, fingerPrint)); + } + public string RootPath { get; set; } public Dictionary InternalLightningByCryptoCode { get; set; } = new Dictionary(); public ExternalServices ExternalServicesByCryptoCode { get; set; } = new ExternalServices(); @@ -230,6 +279,7 @@ namespace BTCPayServer.Configuration get; set; } + public List TrustedFingerprints { get; set; } = new List(); public SSHSettings SSHSettings { get; diff --git a/BTCPayServer/Configuration/DefaultConfiguration.cs b/BTCPayServer/Configuration/DefaultConfiguration.cs index dd68a924d..62b544f9f 100644 --- a/BTCPayServer/Configuration/DefaultConfiguration.cs +++ b/BTCPayServer/Configuration/DefaultConfiguration.cs @@ -38,6 +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); foreach (var network in provider.GetAll()) { var crypto = network.CryptoCode.ToLowerInvariant(); diff --git a/BTCPayServer/Controllers/ServerController.cs b/BTCPayServer/Controllers/ServerController.cs index 240f1e36b..baae1d691 100644 --- a/BTCPayServer/Controllers/ServerController.cs +++ b/BTCPayServer/Controllers/ServerController.cs @@ -260,6 +260,29 @@ namespace BTCPayServer.Controllers ssh = $"sudo bash -c '. /etc/profile.d/btcpay-env.sh && nohup {ssh} > /dev/null 2>&1 & disown'"; var sshClient = _Options.SSHSettings == null ? vm.CreateSSHClient(this.Request.Host.Host) : new SshClient(_Options.SSHSettings.CreateConnectionInfo()); + + if (_Options.TrustedFingerprints.Count != 0) + { + sshClient.HostKeyReceived += (object sender, Renci.SshNet.Common.HostKeyEventArgs e) => + { + 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)}\""); + e.CanTrust = true; // Not a typo, we want the connection to succeed with a warning + } + else + { + e.CanTrust = _Options.IsTrustedFingerprint(e.FingerPrint); + if(!e.CanTrust) + Logs.Configuration.LogError($"SSH host fingerprint for {e.HostKey} is untrusted, start BTCPay with -sshtrustedfingerprints \"{Encoders.Hex.EncodeData(e.FingerPrint)}\""); + } + }; + } + else + { + + } + try { sshClient.Connect(); diff --git a/BTCPayServer/HostedServices/CheckConfigurationHostedService.cs b/BTCPayServer/HostedServices/CheckConfigurationHostedService.cs index ad6d38e18..958705958 100644 --- a/BTCPayServer/HostedServices/CheckConfigurationHostedService.cs +++ b/BTCPayServer/HostedServices/CheckConfigurationHostedService.cs @@ -10,6 +10,7 @@ using Microsoft.Extensions.Hosting; using System.Threading; using BTCPayServer.Configuration; using BTCPayServer.Logging; +using NBitcoin.DataEncoders; namespace BTCPayServer.HostedServices { @@ -30,6 +31,14 @@ namespace BTCPayServer.HostedServices { Logs.Configuration.LogInformation($"SSH settings detected, testing connection to {_options.SSHSettings.Username}@{_options.SSHSettings.Server} on port {_options.SSHSettings.Port} ..."); var connection = new Renci.SshNet.SshClient(_options.SSHSettings.CreateConnectionInfo()); + connection.HostKeyReceived += (object sender, Renci.SshNet.Common.HostKeyEventArgs e) => + { + e.CanTrust = true; + if (!_options.IsTrustedFingerprint(e.FingerPrint)) + { + Logs.Configuration.LogWarning($"SSH host fingerprint for {e.HostKey} is untrusted, start BTCPay with -sshtrustedfingerprints \"{Encoders.Hex.EncodeData(e.FingerPrint)}\""); + } + }; try { connection.Connect();