Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions bitwarden-server.sln
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Sso.IntegrationTest", "bitw
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Server.IntegrationTest", "test\Server.IntegrationTest\Server.IntegrationTest.csproj", "{E75E1F10-BC6F-4EB1-BA75-D897C45AEA0D}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Setup.Test", "test\Setup.Test\Setup.Test.csproj", "{5A3AB73D-F0E8-4DC6-B072-0D3B394621ED}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand Down Expand Up @@ -379,6 +381,10 @@ Global
{E75E1F10-BC6F-4EB1-BA75-D897C45AEA0D}.Debug|Any CPU.Build.0 = Debug|Any CPU
{E75E1F10-BC6F-4EB1-BA75-D897C45AEA0D}.Release|Any CPU.ActiveCfg = Release|Any CPU
{E75E1F10-BC6F-4EB1-BA75-D897C45AEA0D}.Release|Any CPU.Build.0 = Release|Any CPU
{5A3AB73D-F0E8-4DC6-B072-0D3B394621ED}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{5A3AB73D-F0E8-4DC6-B072-0D3B394621ED}.Debug|Any CPU.Build.0 = Debug|Any CPU
{5A3AB73D-F0E8-4DC6-B072-0D3B394621ED}.Release|Any CPU.ActiveCfg = Release|Any CPU
{5A3AB73D-F0E8-4DC6-B072-0D3B394621ED}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand Down Expand Up @@ -440,6 +446,7 @@ Global
{7D98784C-C253-43FB-9873-25B65C6250D6} = {287CFF34-BBDB-4BC4-AF88-1E19A5A4679B}
{FFB09376-595B-6F93-36F0-70CAE90AFECB} = {287CFF34-BBDB-4BC4-AF88-1E19A5A4679B}
{E75E1F10-BC6F-4EB1-BA75-D897C45AEA0D} = {DD5BD056-4AAE-43EF-BBD2-0B569B8DA84F}
{5A3AB73D-F0E8-4DC6-B072-0D3B394621ED} = {DD5BD056-4AAE-43EF-BBD2-0B569B8DA84F}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {E01CBF68-2E20-425F-9EDB-E0A6510CA92F}
Expand Down
124 changes: 124 additions & 0 deletions test/Setup.Test/ProgramTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
ο»Ώusing System.Net;
using System.Net.Http.Json;
using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;
using Bit.Setup;
using NSubstitute;
using RichardSzalay.MockHttp;

namespace Setup.Test;

public class ProgramTests
{
[Fact(Explicit = true)]
public async Task Install_Works()
{
var tempDir = Directory.CreateTempSubdirectory();
try
{
var installationId = $"{Guid.NewGuid()}";
var testApp = Substitute.For<Application>();
testApp.RootDirectory.Returns(tempDir.FullName);
testApp
.ReadInput(Arg.Any<string>())
.Returns(c =>
{
var prompt = c.Arg<string>();
return prompt switch
{
"Enter your installation id (get at https://bitwarden.com/host)" => installationId,
"Enter your installation key" => "test-key",
"Enter your region (US/EU) [US]" => "",
_ => throw new NotImplementedException($"Prompt not configured: {prompt}"),
};
});
testApp
.ReadQuestion(Arg.Any<string>())
.Returns(c =>
{
var prompt = c.Arg<string>();
return prompt switch
{
"Do you have a SSL certificate to use?" => false,
"Do you want to generate a self-signed SSL certificate?" => true,
_ => throw new NotImplementedException(prompt),
};
});

var mockHandler = new MockHttpMessageHandler();

mockHandler
.Expect(HttpMethod.Get, $"https://api.bitwarden.com/installations/{installationId}")
.Respond(HttpStatusCode.OK, JsonContent.Create(new { enabled = true, }));

testApp
.GetHttpClient()
.Returns(mockHandler.ToHttpClient());

Program.MainCore([
"-install", "1",
"-domain", "example.com",
"-letsencrypt", "n",
"-os", "lin",
"-corev", "test-version-does-not-exist",
"-webv", "test-version-does-not-exist",
"-dbname", "test-db",
"-keyconnectorv", "test-version-does-not-exist",
], testApp);

// Assert SSL certificate details
var baseDir = Path.Join(tempDir.FullName, "ssl", "self", "example.com");
var certFile = new FileInfo(Path.Join(baseDir, "certificate.crt"));
Assert.True(certFile.Exists);
var cert = new X509Certificate2(certFile.FullName);

var hundredYearsFromNow = DateTime.UtcNow.AddDays(36500);

Assert.InRange(cert.NotAfter, hundredYearsFromNow.AddMinutes(-1), hundredYearsFromNow.AddMinutes(1));

Assert.Equal("sha256RSA", cert.SignatureAlgorithm.FriendlyName);

var names = cert.SubjectName.EnumerateRelativeDistinguishedNames().ToList();

Assert.Equal(6, names.Count);

Assert.Contains(names, n => n.GetSingleElementType().FriendlyName == "C" && n.GetSingleElementValue() == "US");
Assert.Contains(names, n => n.GetSingleElementType().FriendlyName == "S" && n.GetSingleElementValue() == "California");
Assert.Contains(names, n => n.GetSingleElementType().FriendlyName == "L" && n.GetSingleElementValue() == "Santa Barbara");
Assert.Contains(names, n => n.GetSingleElementType().FriendlyName == "O" && n.GetSingleElementValue() == "Bitwarden Inc.");
Assert.Contains(names, n => n.GetSingleElementType().FriendlyName == "OU" && n.GetSingleElementValue() == "Bitwarden");
Assert.Contains(names, n => n.GetSingleElementType().FriendlyName == "CN" && n.GetSingleElementValue() == "example.com");

Assert.Equal(3, cert.Extensions.Count);
var san = Assert.Single(cert.Extensions.OfType<X509SubjectAlternativeNameExtension>());
var dns = Assert.Single(san.EnumerateDnsNames());
Assert.Equal("example.com", dns);
Assert.Empty(san.EnumerateIPAddresses());
Assert.False(san.Critical);

var basicConstraints = Assert.Single(cert.Extensions.OfType<X509BasicConstraintsExtension>());
Assert.True(basicConstraints.CertificateAuthority);
Assert.False(basicConstraints.HasPathLengthConstraint);
Assert.Equal(0, basicConstraints.PathLengthConstraint);
Assert.False(basicConstraints.Critical);

var subjectKeyIdentifier = Assert.Single(cert.Extensions.OfType<X509SubjectKeyIdentifierExtension>());
Assert.False(subjectKeyIdentifier.Critical);

// Validate that the private key can be imported in PEM format
using var rsa = RSA.Create();
rsa.ImportFromPem(
await File.ReadAllTextAsync(Path.Combine(baseDir, "private.key"), TestContext.Current.CancellationToken)
);

Assert.Equal(4096, rsa.KeySize);
Assert.Equal("RSA", rsa.KeyExchangeAlgorithm);

// Assert other things
}
finally
{
tempDir.Delete(true);
}
}
}
37 changes: 37 additions & 0 deletions test/Setup.Test/Setup.Test.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<OutputType>Exe</OutputType>
<RootNamespace>Setup.Test</RootNamespace>
<TargetFramework>net8.0</TargetFramework>
<!--
This template uses native xUnit.net command line options when using 'dotnet run' and
VSTest by default when using 'dotnet test'. For more information on how to enable support
for Microsoft Testing Platform, please visit:
https://xunit.net/docs/getting-started/v3/microsoft-testing-platform
-->
</PropertyGroup>

<ItemGroup>
<Content Include="xunit.runner.json" CopyToOutputDirectory="PreserveNewest" />
</ItemGroup>

<ItemGroup>
<Using Include="Xunit" />
</ItemGroup>

<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="18.0.1" />
<PackageReference Include="xunit.v3" Version="3.2.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="3.1.5" />
<PackageReference Include="NSubstitute" Version="$(NSubstituteVersion)" />
<PackageReference Include="Kralizek.AutoFixture.Extensions.MockHttp" Version="2.2.1" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\..\util\Setup\Setup.csproj" />
</ItemGroup>

</Project>
3 changes: 3 additions & 0 deletions test/Setup.Test/xunit.runner.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"$schema": "https://xunit.net/schema/current/xunit.runner.schema.json"
}
4 changes: 2 additions & 2 deletions util/Setup/AppIdBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,9 @@ public void Build()

// Needed for backwards compatability with migrated U2F tokens.
Helpers.WriteLine(_context, "Building FIDO U2F app id.");
Directory.CreateDirectory("/bitwarden/web/");
Directory.CreateDirectory($"{_context.App.RootDirectory}/web/");
var template = Helpers.ReadTemplate("AppId");
using (var sw = File.CreateText("/bitwarden/web/app-id.json"))
using (var sw = File.CreateText($"{_context.App.RootDirectory}/web/app-id.json"))
{
sw.Write(template(model));
}
Expand Down
26 changes: 13 additions & 13 deletions util/Setup/CertBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -29,17 +29,17 @@ public void BuildForInstall()

if (!skipSSL)
{
_context.Config.Ssl = Helpers.ReadQuestion("Do you have a SSL certificate to use?");
_context.Config.Ssl = _context.App.ReadQuestion("Do you have a SSL certificate to use?");
if (_context.Config.Ssl)
{
Directory.CreateDirectory($"/bitwarden/ssl/{_context.Install.Domain}/");
Directory.CreateDirectory($"{_context.App.RootDirectory}/ssl/{_context.Install.Domain}/");
var message = "Make sure 'certificate.crt' and 'private.key' are provided in the \n" +
"appropriate directory before running 'start' (see docs for info).";
Helpers.ShowBanner(_context, "NOTE", message);
}
else if (Helpers.ReadQuestion("Do you want to generate a self-signed SSL certificate?"))
else if (_context.App.ReadQuestion("Do you want to generate a self-signed SSL certificate?"))
{
Directory.CreateDirectory($"/bitwarden/ssl/self/{_context.Install.Domain}/");
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;
Expand All @@ -58,22 +58,22 @@ public void BuildForInstall()
{
_context.Install.Trusted = true;
_context.Install.DiffieHellman = true;
Directory.CreateDirectory($"/bitwarden/letsencrypt/live/{_context.Install.Domain}/");
Directory.CreateDirectory($"{_context.App.RootDirectory}/letsencrypt/live/{_context.Install.Domain}/");
Helpers.Exec($"openssl dhparam -out " +
$"/bitwarden/letsencrypt/live/{_context.Install.Domain}/dhparam.pem 2048");
$"{_context.App.RootDirectory}/letsencrypt/live/{_context.Install.Domain}/dhparam.pem 2048");
}
else if (_context.Config.Ssl && !_context.Install.SelfSignedCert)
{
_context.Install.Trusted = Helpers.ReadQuestion("Is this a trusted SSL certificate " +
_context.Install.Trusted = _context.App.ReadQuestion("Is this a trusted SSL certificate " +
"(requires ca.crt, see docs)?");
}

Helpers.WriteLine(_context, "Generating key for IdentityServer.");
_context.Install.IdentityCertPassword = Helpers.SecureRandomString(32, alpha: true, numeric: true);
Directory.CreateDirectory("/bitwarden/identity/");
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 /bitwarden/identity/identity.pfx -inkey identity.key " +
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);
Expand All @@ -97,14 +97,14 @@ public void BuildForInstall()

public void BuildForUpdater()
{
if (_context.Config.EnableKeyConnector && !File.Exists("/bitwarden/key-connector/bwkc.pfx"))
if (_context.Config.EnableKeyConnector && !File.Exists($"{_context.App.RootDirectory}/key-connector/bwkc.pfx"))
{
Directory.CreateDirectory("/bitwarden/key-connector/");
var keyConnectorCertPassword = Helpers.GetValueFromEnvFile("key-connector",
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 /bitwarden/key-connector/bwkc.pfx -inkey bwkc.key " +
Helpers.Exec($"openssl pkcs12 -export -out {_context.App.RootDirectory}/key-connector/bwkc.pfx -inkey bwkc.key " +
$"-in bwkc.crt -passout pass:{keyConnectorCertPassword}");
}
}
Expand Down
14 changes: 8 additions & 6 deletions util/Setup/Context.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ namespace Bit.Setup;

public class Context
{
private const string ConfigPath = "/bitwarden/config.yml";
private string ConfigPath => $"{App.RootDirectory}/config.yml";

// These track of old CSP default values to correct.
// Do not change these values.
Expand Down Expand Up @@ -45,6 +45,8 @@ public class Context
Jan2023ContentSecurityPolicy
};

public required Application App { get; init; }

public string[] Args { get; set; }
public bool Quiet { get; set; }
public bool Stub { get; set; }
Expand All @@ -69,18 +71,18 @@ public void LoadConfiguration()
Helpers.WriteLine(this, "No existing `config.yml` detected. Let's generate one.");

// Looks like updating from older version. Try to create config file.
var url = Helpers.GetValueFromEnvFile("global", "globalSettings__baseServiceUri__vault");
var url = Helpers.GetValueFromEnvFile(App, "global", "globalSettings__baseServiceUri__vault");
if (!Uri.TryCreate(url, UriKind.Absolute, out var uri))
{
Helpers.WriteLine(this, "Unable to determine existing installation url.");
return;
}
Config.Url = url;

var push = Helpers.GetValueFromEnvFile("global", "globalSettings__pushRelayBaseUri");
var push = Helpers.GetValueFromEnvFile(App, "global", "globalSettings__pushRelayBaseUri");
Config.PushNotifications = push != "REPLACE";

var composeFile = "/bitwarden/docker/docker-compose.yml";
var composeFile = $"{App.RootDirectory}/docker/docker-compose.yml";
if (File.Exists(composeFile))
{
var fileLines = File.ReadAllLines(composeFile);
Expand Down Expand Up @@ -118,7 +120,7 @@ public void LoadConfiguration()
}
}

var nginxFile = "/bitwarden/nginx/default.conf";
var nginxFile = $"{App.RootDirectory}/nginx/default.conf";
if (File.Exists(nginxFile))
{
var confContent = File.ReadAllText(nginxFile);
Expand Down Expand Up @@ -177,7 +179,7 @@ public void SaveConfiguration()
.WithEmissionPhaseObjectGraphVisitor(args => new CommentsObjectGraphVisitor(args.InnerVisitor))
.Build();
var yaml = serializer.Serialize(Config);
Directory.CreateDirectory("/bitwarden/");
Directory.CreateDirectory($"{App.RootDirectory}/");
using (var sw = File.CreateText(ConfigPath))
{
sw.Write(yaml);
Expand Down
4 changes: 2 additions & 2 deletions util/Setup/DockerComposeBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ public void BuildForUpdater()

private void Build()
{
Directory.CreateDirectory("/bitwarden/docker/");
Directory.CreateDirectory($"{_context.App.RootDirectory}/docker/");
Helpers.WriteLine(_context, "Building docker-compose.yml.");
if (!_context.Config.GenerateComposeConfig)
{
Expand All @@ -32,7 +32,7 @@ private void Build()

var template = Helpers.ReadTemplate("DockerCompose");
var model = new TemplateModel(_context);
using (var sw = File.CreateText("/bitwarden/docker/docker-compose.yml"))
using (var sw = File.CreateText($"{_context.App.RootDirectory}/docker/docker-compose.yml"))
{
sw.Write(template(model));
}
Expand Down
Loading
Loading