diff --git a/test/Setup.Test/ProgramTests.cs b/test/Setup.Test/ProgramTests.cs index 256bcc68be9b..401930a338ea 100644 --- a/test/Setup.Test/ProgramTests.cs +++ b/test/Setup.Test/ProgramTests.cs @@ -72,7 +72,7 @@ public async Task Install_Works() Assert.True(certFile.Exists); var cert = new X509Certificate2(certFile.FullName); - var hundredYearsFromNow = DateTime.UtcNow.AddDays(36500); + var hundredYearsFromNow = DateTime.Now.AddDays(36500); Assert.InRange(cert.NotAfter, hundredYearsFromNow.AddMinutes(-1), hundredYearsFromNow.AddMinutes(1)); diff --git a/util/Setup/CertBuilder.cs b/util/Setup/CertBuilder.cs index ff8aac25b025..f5e9d8ad76d6 100644 --- a/util/Setup/CertBuilder.cs +++ b/util/Setup/CertBuilder.cs @@ -39,17 +39,39 @@ public void BuildForInstall() } else if (_context.App.ReadQuestion("Do you want to generate a self-signed SSL certificate?")) { - Directory.CreateDirectory($"{_context.App.RootDirectory}/ssl/self/{_context.Install.Domain}/"); + var directory = Directory.CreateDirectory($"{_context.App.RootDirectory}/ssl/self/{_context.Install.Domain}/"); Helpers.WriteLine(_context, "Generating self signed SSL certificate."); _context.Config.Ssl = true; _context.Install.Trusted = false; _context.Install.SelfSignedCert = true; - Helpers.Exec("openssl req -x509 -newkey rsa:4096 -sha256 -nodes -days 36500 " + - $"-keyout /bitwarden/ssl/self/{_context.Install.Domain}/private.key " + - $"-out /bitwarden/ssl/self/{_context.Install.Domain}/certificate.crt " + - $"-reqexts SAN -extensions SAN " + - $"-config <(cat /usr/lib/ssl/openssl.cnf <(printf '[SAN]\nsubjectAltName=DNS:{_context.Install.Domain}\nbasicConstraints=CA:true')) " + - $"-subj \"/C=US/ST=California/L=Santa Barbara/O=Bitwarden Inc./OU=Bitwarden/CN={_context.Install.Domain}\""); + var opensslConfig = File.ReadAllText("/usr/lib/ssl/openssl.cnf") + + $"\n[SAN]\nsubjectAltName=DNS:{_context.Install.Domain}\nbasicConstraints=CA:true"; + var opensslConfigPath = Path.GetTempFileName(); + + try + { + File.WriteAllText(opensslConfigPath, opensslConfig); + Helpers.Exec( + "openssl", + [ + "req", + "-x509", + "-newkey", "rsa:4096", + "-sha256", + "-nodes", + "-days", "36500", + "-keyout", $"{directory.FullName}/private.key", + "-out", $"{directory.FullName}/certificate.crt", + "-reqexts", "SAN", + "-extensions", "SAN", + "-config", opensslConfigPath, + "-subj", $"/C=US/ST=California/L=Santa Barbara/O=Bitwarden Inc./OU=Bitwarden/CN={_context.Install.Domain}", + ]); + } + finally + { + File.Delete(opensslConfigPath); + } } } } @@ -58,9 +80,14 @@ public void BuildForInstall() { _context.Install.Trusted = true; _context.Install.DiffieHellman = true; - Directory.CreateDirectory($"{_context.App.RootDirectory}/letsencrypt/live/{_context.Install.Domain}/"); - Helpers.Exec($"openssl dhparam -out " + - $"{_context.App.RootDirectory}/letsencrypt/live/{_context.Install.Domain}/dhparam.pem 2048"); + var directory = Directory.CreateDirectory($"{_context.App.RootDirectory}/letsencrypt/live/{_context.Install.Domain}/"); + Helpers.Exec( + "openssl", + [ + "dhparam", + "-out", $"{directory.FullName}/dhparam.pem", + "2048", + ]); } else if (_context.Config.Ssl && !_context.Install.SelfSignedCert) { @@ -71,10 +98,29 @@ public void BuildForInstall() Helpers.WriteLine(_context, "Generating key for IdentityServer."); _context.Install.IdentityCertPassword = Helpers.SecureRandomString(32, alpha: true, numeric: true); Directory.CreateDirectory($"{_context.App.RootDirectory}/identity/"); - Helpers.Exec("openssl req -x509 -newkey rsa:4096 -sha256 -nodes -keyout identity.key " + - "-out identity.crt -subj \"/CN=Bitwarden IdentityServer\" -days 36500"); - Helpers.Exec($"openssl pkcs12 -export -out {_context.App.RootDirectory}/identity/identity.pfx -inkey identity.key " + - $"-in identity.crt -passout pass:{_context.Install.IdentityCertPassword}"); + Helpers.Exec( + "openssl", + [ + "req", + "-x509", + "-newkey", "rsa:4096", + "-sha256", + "-nodes", + "-keyout", "identity.key", + "-out", "identity.crt", + "-subj", "/CN=Bitwarden IdentityServer", + "-days", "36500", + ]); + Helpers.Exec( + "openssl", + [ + "pkcs12", + "-export", + "-out", $"{_context.App.RootDirectory}/identity/identity.pfx", + "-inkey", "identity.key", + "-in", "identity.crt", + "-passout", $"pass:{_context.Install.IdentityCertPassword}", + ]); Helpers.WriteLine(_context); @@ -102,10 +148,29 @@ public void BuildForUpdater() Directory.CreateDirectory($"{_context.App.RootDirectory}/key-connector/"); var keyConnectorCertPassword = Helpers.GetValueFromEnvFile(_context.App, "key-connector", "keyConnectorSettings__certificate__filesystemPassword"); - Helpers.Exec("openssl req -x509 -newkey rsa:4096 -sha256 -nodes -keyout bwkc.key " + - "-out bwkc.crt -subj \"/CN=Bitwarden Key Connector\" -days 36500"); - Helpers.Exec($"openssl pkcs12 -export -out {_context.App.RootDirectory}/key-connector/bwkc.pfx -inkey bwkc.key " + - $"-in bwkc.crt -passout pass:{keyConnectorCertPassword}"); + Helpers.Exec( + "openssl", + [ + "req", + "-x509", + "-newkey", "rsa:4096", + "-sha256", + "-nodes", + "-keyout", "bwkc.key", + "-out", "bwkc.crt", + "-subj", "/CN=Bitwarden Key Connector", + "-days", "36500", + ]); + Helpers.Exec( + "openssl", + [ + "pkcs12", + "-export", + "-out", $"{_context.App.RootDirectory}/key-connector/bwkc.pfx", + "-inkey", "bwkc.key", + "-in", "bwkc.crt", + "-passout", $"pass:{keyConnectorCertPassword}" + ]); } } } diff --git a/util/Setup/EnvironmentFileBuilder.cs b/util/Setup/EnvironmentFileBuilder.cs index 50e6650546bf..79d98a90c251 100644 --- a/util/Setup/EnvironmentFileBuilder.cs +++ b/util/Setup/EnvironmentFileBuilder.cs @@ -178,13 +178,13 @@ private void Build() { sw.Write(template(new TemplateModel(_globalValues))); } - Helpers.Exec($"chmod 600 {_context.App.RootDirectory}/docker/global.env"); + Helpers.Exec("chmod", ["600", $"{_context.App.RootDirectory}/docker/global.env"]); using (var sw = File.CreateText($"{_context.App.RootDirectory}/docker/mssql.env")) { sw.Write(template(new TemplateModel(_mssqlValues))); } - Helpers.Exec($"chmod 600 {_context.App.RootDirectory}/docker/mssql.env"); + Helpers.Exec("chmod", ["600", $"{_context.App.RootDirectory}/docker/mssql.env"]); Helpers.WriteLine(_context, "Building docker environment override files."); Directory.CreateDirectory($"{_context.App.RootDirectory}/env/"); @@ -192,13 +192,13 @@ private void Build() { sw.Write(template(new TemplateModel(_globalOverrideValues))); } - Helpers.Exec($"chmod 600 {_context.App.RootDirectory}/env/global.override.env"); + Helpers.Exec("chmod", ["600", $"{_context.App.RootDirectory}/env/global.override.env"]); using (var sw = File.CreateText($"{_context.App.RootDirectory}/env/mssql.override.env")) { sw.Write(template(new TemplateModel(_mssqlOverrideValues))); } - Helpers.Exec($"chmod 600 {_context.App.RootDirectory}/env/mssql.override.env"); + Helpers.Exec("chmod", ["600", $"{_context.App.RootDirectory}/env/mssql.override.env"]); if (_context.Config.EnableKeyConnector) { @@ -207,7 +207,7 @@ private void Build() sw.Write(template(new TemplateModel(_keyConnectorOverrideValues))); } - Helpers.Exec($"chmod 600 {_context.App.RootDirectory}/env/key-connector.override.env"); + Helpers.Exec("chmod", ["600", $"{_context.App.RootDirectory}/env/key-connector.override.env"]); } // Empty uid env file. Only used on Linux hosts. diff --git a/util/Setup/Helpers.cs b/util/Setup/Helpers.cs index 40cf8fa8277a..34dd2f17dd3e 100644 --- a/util/Setup/Helpers.cs +++ b/util/Setup/Helpers.cs @@ -3,7 +3,6 @@ using System.Diagnostics; using System.Reflection; -using System.Runtime.InteropServices; using System.Security.Cryptography; using System.Text; @@ -112,35 +111,41 @@ public static string GetValueFromEnvFile(Application config, string envFile, str return null; } - public static string Exec(string cmd, bool returnStdout = false) + public static string Exec(string cmd, string[] arguments = null, bool returnStdout = false, bool returnStderr = false) { - var process = new Process + var startInfo = new ProcessStartInfo(cmd, arguments ?? []) { - StartInfo = new ProcessStartInfo - { - RedirectStandardOutput = true, - UseShellExecute = false, - CreateNoWindow = true, - WindowStyle = ProcessWindowStyle.Hidden - } + RedirectStandardOutput = returnStdout, + RedirectStandardError = returnStderr, + UseShellExecute = false, + CreateNoWindow = true, + WindowStyle = ProcessWindowStyle.Hidden }; - if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + var process = new Process { StartInfo = startInfo }; + + var result = new StringBuilder(); + + process.OutputDataReceived += (_, e) => { - var escapedArgs = cmd.Replace("\"", "\\\""); - process.StartInfo.FileName = "/bin/sh"; - process.StartInfo.Arguments = $"-c \"{escapedArgs}\""; - } - else + if (!returnStdout || e.Data == null) return; + result.AppendLine(e.Data); + }; + + process.ErrorDataReceived += (_, e) => { - process.StartInfo.FileName = "powershell"; - process.StartInfo.Arguments = cmd; - } + if (!returnStderr || e.Data == null) return; + result.AppendLine(e.Data); + }; process.Start(); - var result = returnStdout ? process.StandardOutput.ReadToEnd() : null; + + if (returnStdout) process.BeginOutputReadLine(); + if (returnStderr) process.BeginErrorReadLine(); + process.WaitForExit(); - return result; + + return result.ToString(); } public static string ReadInput(string prompt) diff --git a/util/Setup/Program.cs b/util/Setup/Program.cs index f7fa3ebe0a38..1ab83369ae4e 100644 --- a/util/Setup/Program.cs +++ b/util/Setup/Program.cs @@ -96,6 +96,11 @@ private static void Install(Application application) if (_context.Parameters.TryGetValue("domain", out var domain)) { _context.Install.Domain = domain.ToLowerInvariant(); + if (Uri.CheckHostName(_context.Install.Domain) != UriHostNameType.Dns) + { + Helpers.WriteError("Domain is invalid. A valid domain name is required (e.g. bitwarden.example.com)."); + return; + } } if (_context.Parameters.TryGetValue("dbname", out var database)) { @@ -159,19 +164,65 @@ private static void Update(Application application) // a new cert and bag to replace the old Identity.pfx. This fixes an issue that came up as a result of // moving the project to .NET 5. _context.Install.IdentityCertPassword = Helpers.GetValueFromEnvFile(_context.App, "global", "globalSettings__identityServer__certificatePassword"); - var certCountString = Helpers.Exec($"openssl pkcs12 -nokeys -info -in {application.RootDirectory}/identity/identity.pfx " + - $"-passin pass:{_context.Install.IdentityCertPassword} 2> /dev/null | grep -c \"\\-----BEGIN CERTIFICATE----\"", true); - if (int.TryParse(certCountString, out var certCount) && certCount > 1) + var certOutput = Helpers.Exec( + "openssl", + [ + "pkcs12", + "-nokeys", + "-info", + "-in", $"{application.RootDirectory}/identity/identity.pfx", + "-passin", $"pass:{_context.Install.IdentityCertPassword}", + ], + returnStdout: true, + returnStderr: false); + + var certCount = certOutput + .Split(Environment.NewLine, StringSplitOptions.RemoveEmptyEntries) + .Count(line => line.Contains("-----BEGIN CERTIFICATE-----")); + + if (certCount > 1) { // Extract key from identity.pfx - Helpers.Exec($"openssl pkcs12 -in {application.RootDirectory}/identity/identity.pfx -nocerts -nodes -out identity.key " + - $"-passin pass:{_context.Install.IdentityCertPassword} > /dev/null 2>&1"); + Helpers.Exec( + "openssl", + [ + "pkcs12", + "-in", $"{application.RootDirectory}/identity/identity.pfx", + "-nocerts", + "-nodes", + "-out", "identity.key", + "-passin", $"pass:{_context.Install.IdentityCertPassword}", + ], + returnStdout: false, + returnStderr: false); + // Extract certificate from identity.pfx - Helpers.Exec($"openssl pkcs12 -in {application.RootDirectory}/identity/identity.pfx -clcerts -nokeys -out identity.crt " + - $"-passin pass:{_context.Install.IdentityCertPassword} > /dev/null 2>&1"); + Helpers.Exec( + "openssl", + [ + "pkcs12", + "-in", $"{application.RootDirectory}/identity/identity.pfx", + "-clcerts", + "-nokeys", + "-out", "identity.crt", + "-passin", $"pass:{_context.Install.IdentityCertPassword}", + ], + returnStdout: false, + returnStderr: false); + // Create new PKCS12 bag with certificate and key - Helpers.Exec($"openssl pkcs12 -export -out {application.RootDirectory}/identity/identity.pfx -inkey identity.key " + - $"-in identity.crt -passout pass:{_context.Install.IdentityCertPassword} > /dev/null 2>&1"); + Helpers.Exec( + "openssl", + [ + "pkcs12", + "-export", + "-out", $"{application.RootDirectory}/identity/identity.pfx", + "-inkey", "identity.key", + "-in", $"identity.crt", + "-passout", $"pass:{_context.Install.IdentityCertPassword}" + ], + returnStdout: false, + returnStderr: false); } if (_context.Parameters.ContainsKey("db"))