diff --git a/.gitlab/e2e/e2e.yml b/.gitlab/e2e/e2e.yml index 5ebc7fd5906b58..9e44a3d43614c4 100644 --- a/.gitlab/e2e/e2e.yml +++ b/.gitlab/e2e/e2e.yml @@ -218,6 +218,22 @@ new-e2e-fips-compliance-test: TARGETS: ./tests/fips-compliance TEAM: agent-runtimes +new-e2e-windows-fips-compliance-test: + extends: .new_e2e_template + needs: + - !reference [.needs_new_e2e_template] + - qa_agent_fips + - deploy_windows_testing-a7-fips + rules: + - !reference [.on_arun_or_e2e_changes] + - !reference [.manual] + variables: + TARGETS: ./tests/fips-compliance + TEAM: windows-agent + parallel: + matrix: + - EXTRA_PARAMS: --run "TestWindowsVM$" + new-e2e-windows-service-test: extends: .new_e2e_template needs: diff --git a/.gitlab/e2e_install_packages/windows.yml b/.gitlab/e2e_install_packages/windows.yml index 63912aab6b7870..a911d61abbfc13 100644 --- a/.gitlab/e2e_install_packages/windows.yml +++ b/.gitlab/e2e_install_packages/windows.yml @@ -123,6 +123,7 @@ new-e2e-windows-agent-a7-x86_64-fips: parallel: matrix: - EXTRA_PARAMS: --run "TestFIPSAgent$" + - EXTRA_PARAMS: --run "TestFIPSAgentAltDir$" - EXTRA_PARAMS: --run "TestFIPSAgentDoesNotInstallOverAgent$" - EXTRA_PARAMS: --run "TestAgentDoesNotInstallOverFIPSAgent$" rules: diff --git a/Dockerfiles/agent/install.ps1 b/Dockerfiles/agent/install.ps1 index a00bdb738091f1..c175ae6bdc431a 100755 --- a/Dockerfiles/agent/install.ps1 +++ b/Dockerfiles/agent/install.ps1 @@ -52,6 +52,27 @@ foreach ($s in $services.Keys) { Install-Service -SvcName $s -BinPath $services[$s][0] $services[$s][1] } +# Since OpenSSL 3.4, the install paths can be retrieved from the registry instead of being hardcoded at build time. +# https://github.com/openssl/openssl/blob/master/NOTES-WINDOWS.md#installation-directories +# TODO: How best to configure the OpenSSL version? +$opensslVersion = "3.4" +if ($env:WITH_FIPS -eq "true") { + $opensslctx = "datadog-fips-agent" +} else { + $opensslctx = "datadog-agent" +} +$keyPath = "HKLM:\SOFTWARE\Wow6432Node\OpenSSL-$opensslVersion-$opensslctx" + +# Create the registry key +if (-not (Test-Path $keyPath)) { + New-Item -Path $keyPath -Force +} + +# Set the registry values +$embeddedPath = "C:\Program Files\Datadog\Datadog Agent\embedded3" +Set-ItemProperty -Path $keyPath -Name "OPENSSLDIR" -Value "$embeddedPath\ssl" +Set-ItemProperty -Path $keyPath -Name "ENGINESDIR" -Value "$embeddedPath\lib\engines-3" +Set-ItemProperty -Path $keyPath -Name "MODULESDIR" -Value "$embeddedPath\lib\ossl-modules" # Allow to run agent binaries as `agent` setx /m PATH "$Env:Path;C:/Program Files/Datadog/Datadog Agent/bin;C:/Program Files/Datadog/Datadog Agent/bin/agent" diff --git a/tasks/msi.py b/tasks/msi.py index e03c2c527d2772..42a7aa13fb8ac8 100644 --- a/tasks/msi.py +++ b/tasks/msi.py @@ -72,16 +72,23 @@ def _get_vs_build_command(cmd, vstudio_root=None): return cmd -def _get_env(ctx, major_version='7', release_version='nightly'): +def _get_env(ctx, major_version='7', release_version='nightly-a7', flavor=None): env = load_release_versions(ctx, release_version) + if flavor is None: + flavor = os.getenv("AGENT_FLAVOR", "") + env['PACKAGE_VERSION'] = get_version( ctx, include_git=True, url_safe=True, major_version=major_version, include_pipeline_id=True ) - env['AGENT_FLAVOR'] = os.getenv("AGENT_FLAVOR", "") + env['AGENT_FLAVOR'] = flavor env['AGENT_INSTALLER_OUTPUT_DIR'] = BUILD_OUTPUT_DIR env['NUGET_PACKAGES_DIR'] = NUGET_PACKAGES_DIR env['AGENT_PRODUCT_NAME_SUFFIX'] = "" + # Used for installation directories registry keys + # https://github.com/openssl/openssl/blob/master/NOTES-WINDOWS.md#installation-directories + # TODO: How best to configure the OpenSSL version? + env['AGENT_OPENSSL_VERSION'] = "3.4" return env @@ -281,12 +288,19 @@ def _msi_output_name(env): @task def build( - ctx, vstudio_root=None, arch="x64", major_version='7', release_version='nightly', debug=False, build_upgrade=False + ctx, + vstudio_root=None, + arch="x64", + major_version='7', + release_version='nightly-a7', + flavor=None, + debug=False, + build_upgrade=False, ): """ Build the MSI installer for the agent """ - env = _get_env(ctx, major_version, release_version) + env = _get_env(ctx, major_version, release_version, flavor=flavor) env['OMNIBUS_TARGET'] = 'main' configuration = _msbuild_configuration(debug=debug) build_outdir = build_out_dir(arch, configuration) @@ -385,7 +399,7 @@ def build_installer(ctx, vstudio_root=None, arch="x64", debug=False): @task -def test(ctx, vstudio_root=None, arch="x64", major_version='7', release_version='nightly', debug=False): +def test(ctx, vstudio_root=None, arch="x64", major_version='7', release_version='nightly-a7', debug=False): """ Run the unit test for the MSI installer for the agent """ diff --git a/test/new-e2e/pkg/provisioners/aws/host/windows/host.go b/test/new-e2e/pkg/provisioners/aws/host/windows/host.go index 642a1a7dcc2e23..d3180af371da0d 100644 --- a/test/new-e2e/pkg/provisioners/aws/host/windows/host.go +++ b/test/new-e2e/pkg/provisioners/aws/host/windows/host.go @@ -25,6 +25,7 @@ import ( "github.com/DataDog/datadog-agent/test/new-e2e/pkg/utils/e2e/client/agentclientparams" "github.com/DataDog/datadog-agent/test/new-e2e/pkg/utils/optional" "github.com/DataDog/datadog-agent/test/new-e2e/tests/windows/components/defender" + "github.com/DataDog/datadog-agent/test/new-e2e/tests/windows/components/fipsmode" ) const ( @@ -43,6 +44,7 @@ type ProvisionerParams struct { activeDirectoryOptions []activedirectory.Option defenderoptions []defender.Option installerOptions []installer.Option + fipsModeOptions []fipsmode.Option } // ProvisionerOption is a provisioner option. @@ -120,6 +122,16 @@ func WithDefenderOptions(opts ...defender.Option) ProvisionerOption { } } +// WithFIPSModeOptions configures FIPS mode on an EC2 VM. +// +// Ordered before the Agent setup. +func WithFIPSModeOptions(opts ...fipsmode.Option) ProvisionerOption { + return func(params *ProvisionerParams) error { + params.fipsModeOptions = append(params.fipsModeOptions, opts...) + return nil + } +} + // WithInstaller configures Datadog Installer on an EC2 VM. func WithInstaller(opts ...installer.Option) ProvisionerOption { return func(params *ProvisionerParams) error { @@ -231,6 +243,20 @@ func Run(ctx *pulumi.Context, env *environments.WindowsHost, params *Provisioner env.Installer = nil } + if params.fipsModeOptions != nil { + fipsMode, err := fipsmode.New(awsEnv.CommonEnvironment, host, params.fipsModeOptions...) + if err != nil { + return err + } + // We want Agent setup to happen after FIPS mode setup, but only + // because that's the use case we are interested in. + // Ideally the provisioner would allow the user to specify the order of + // the resources, but that's not supported right now. + params.agentOptions = append(params.agentOptions, + agentparams.WithPulumiResourceOptions( + pulumi.DependsOn(fipsMode.Resources))) + } + return nil } @@ -243,6 +269,7 @@ func getProvisionerParams(opts ...ProvisionerOption) *ProvisionerParams { fakeintakeOptions: []fakeintake.Option{}, // Disable Windows Defender on VMs by default defenderoptions: []defender.Option{defender.WithDefenderDisabled()}, + fipsModeOptions: []fipsmode.Option{}, } err := optional.ApplyOptions(params, opts) if err != nil { diff --git a/test/new-e2e/tests/fips-compliance/fips_win_test.go b/test/new-e2e/tests/fips-compliance/fips_win_test.go new file mode 100644 index 00000000000000..7fd4daabc32f80 --- /dev/null +++ b/test/new-e2e/tests/fips-compliance/fips_win_test.go @@ -0,0 +1,200 @@ +// Unless explicitly stated otherwise all files in this repository are licensed +// under the Apache License Version 2.0. +// This product includes software developed at Datadog (https://www.datadoghq.com/). +// Copyright 2024-present Datadog, Inc. + +package fipscompliance + +import ( + _ "embed" + "fmt" + "path/filepath" + "time" + + "github.com/DataDog/test-infra-definitions/components/datadog/agentparams" + + fakeintakeclient "github.com/DataDog/datadog-agent/test/fakeintake/client" + "github.com/DataDog/datadog-agent/test/new-e2e/pkg/e2e" + "github.com/DataDog/datadog-agent/test/new-e2e/pkg/environments" + awsHostWindows "github.com/DataDog/datadog-agent/test/new-e2e/pkg/provisioners/aws/host/windows" + "github.com/DataDog/datadog-agent/test/new-e2e/pkg/utils/e2e/client" + windowsCommon "github.com/DataDog/datadog-agent/test/new-e2e/tests/windows/common" + windowsAgent "github.com/DataDog/datadog-agent/test/new-e2e/tests/windows/common/agent" + "github.com/DataDog/datadog-agent/test/new-e2e/tests/windows/components/fipsmode" + + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +//go:embed fixtures/e2e_fips_test.py +var fipsTestCheck string + +type windowsVMSuite struct { + e2e.BaseSuite[environments.WindowsHost] + + installPath string +} + +// TestWindowsVM tests that the FIPS Agent can report metrics to the fakeintake +func TestWindowsVM(t *testing.T) { + suiteParams := []e2e.SuiteOption{e2e.WithProvisioner(awsHostWindows.Provisioner( + // Enable FIPS mode on the host (done before Agent install) + awsHostWindows.WithFIPSModeOptions(fipsmode.WithFIPSModeEnabled()), + awsHostWindows.WithAgentOptions( + // Use FIPS Agent package + agentparams.WithFlavor(agentparams.FIPSFlavor), + // Install custom check that reports the FIPS mode of Python + // TODO ADXT-881: Need forward slashes to workaround test-infra bug + agentparams.WithFile( + `C:/ProgramData/Datadog/checks.d/e2e_fips_test.py`, + fipsTestCheck, + false, + ), + agentparams.WithFile( + `C:/ProgramData/Datadog/conf.d/e2e_fips_test.yaml`, + ` +init_config: +instances: [{}] +`, + false, + ), + ), + ))} + + e2e.Run(t, &windowsVMSuite{}, suiteParams...) +} + +func (s *windowsVMSuite) SetupSuite() { + s.BaseSuite.SetupSuite() + host := s.Env().RemoteHost + var err error + + s.installPath, err = windowsAgent.GetInstallPathFromRegistry(host) + s.Require().NoError(err) +} + +// TestVersionCommands tests that the version command for each of the Agent binaries +// works when FIPS mode is enabled and panics when GOFIPS=1 AND the system is not in FIPS mode. +func (s *windowsVMSuite) TestVersionCommands() { + host := s.Env().RemoteHost + + windowsCommon.EnableFIPSMode(host) + s.Run("System FIPS Enabled", func() { + s.testAgentBinaries(func(executable string) { + var err error + _, err = s.execAgentCommandWithFIPS(executable, "version") + s.Assert().NoError(err) + _, err = s.execAgentCommand(executable, "version") + s.Assert().NoError(err) + }) + }) + + windowsCommon.DisableFIPSMode(host) + s.Run("System FIPS Disabled", func() { + s.testAgentBinaries(func(executable string) { + var err error + _, err = s.execAgentCommandWithFIPS(executable, "version") + assertErrorContainsFIPSPanic(s.T(), err, "agent should panic when GOFIPS=1 but system FIPS is disabled") + _, err = s.execAgentCommand(executable, "version") + s.Assert().NoError(err) + }) + }) +} + +// TestAgentStatusOutput tests that the Agent status command reports the correct FIPS mode status +func (s *windowsVMSuite) TestAgentStatusOutput() { + host := s.Env().RemoteHost + + windowsCommon.EnableFIPSMode(host) + s.Run("status command", func() { + s.Run("gofips enabled", func() { + status, err := s.execAgentCommandWithFIPS("agent.exe", "status") + require.NoError(s.T(), err) + assert.Contains(s.T(), status, "FIPS Mode: enabled") + }) + + s.Run("gofips disabled", func() { + status, err := s.execAgentCommand("agent.exe", "status") + require.NoError(s.T(), err) + assert.Contains(s.T(), status, "FIPS Mode: enabled", "FIPS Mode should not depend on GOFIPS") + }) + }) + + windowsCommon.DisableFIPSMode(host) + s.Run("status command", func() { + s.Run("gofips disabled", func() { + status, err := s.execAgentCommand("agent.exe", "status") + require.NoError(s.T(), err) + assert.Contains(s.T(), status, "FIPS Mode: disabled") + }) + }) + +} + +// TestReportsFIPSStatusMetrics tests that the custom check from our fixtures +// is able to report metrics while in FIPS mode. These metric values are based +// on the status of Python's FIPS mode. +func (s *windowsVMSuite) TestReportsFIPSStatusMetrics() { + host := s.Env().RemoteHost + // Restart the Agent and reset the aggregator to ensure the metrics are fresh + // with FIPS mode enabled. + err := windowsCommon.StopService(host, "datadogagent") + require.NoError(s.T(), err) + err = s.Env().FakeIntake.Client().FlushServerAndResetAggregators() + require.NoError(s.T(), err) + err = windowsCommon.EnableFIPSMode(host) + require.NoError(s.T(), err) + err = windowsCommon.StartService(host, "datadogagent") + require.NoError(s.T(), err) + + s.EventuallyWithT(func(c *assert.CollectT) { + metrics, err := s.Env().FakeIntake.Client().FilterMetrics("e2e.fips_mode", fakeintakeclient.WithMetricValueHigherThan(0)) + assert.NoError(c, err) + assert.Greater(c, len(metrics), 0, "no 'e2e.fips_mode' with value higher than 0 yet") + + metrics, err = s.Env().FakeIntake.Client().FilterMetrics("e2e.fips_dll_loaded", fakeintakeclient.WithMetricValueHigherThan(0)) + assert.NoError(c, err) + assert.Greater(c, len(metrics), 0, "no 'e2e.fips_dll_loaded' with value higher than 0 yet") + }, 5*time.Minute, 10*time.Second) +} + +// testAgentBinaries runs a subtest for each of the Agent binaries in the install path +func (s *windowsVMSuite) testAgentBinaries(subtest func(executable string)) { + executables := []string{"agent.exe", "agent/system-probe.exe", "agent/trace-agent.exe", + "agent/process-agent.exe", "agent/security-agent.exe"} + for _, executable := range executables { + s.Run(executable, func() { + subtest(executable) + }) + } +} + +func (s *windowsVMSuite) execAgentCommand(executable, command string, options ...client.ExecuteOption) (string, error) { + host := s.Env().RemoteHost + s.Require().NotEmpty(s.installPath) + + agentPath := filepath.Join(s.installPath, "bin", executable) + cmd := fmt.Sprintf(`& "%s" %s`, agentPath, command) + return host.Execute(cmd, options...) +} + +func (s *windowsVMSuite) execAgentCommandWithFIPS(executable, command string) (string, error) { + // There isn't support for appending env vars to client.ExecuteOption, so + // this function doesn't accept any other options. + + // Setting GOFIPS=1 causes the Windows FIPS Agent to panic if the system is not in FIPS mode. + // This setting does NOT control whether the FIPS Agent uses FIPS-compliant crypto libraries, + // the System-level setting determines that. + // https://github.com/microsoft/go/tree/microsoft/main/eng/doc/fips#windows-fips-mode-cng + vars := client.EnvVar{ + "GOFIPS": "1", + } + + return s.execAgentCommand(executable, command, client.WithEnvVariables(vars)) +} + +func assertErrorContainsFIPSPanic(t *testing.T, err error, args ...interface{}) bool { + return assert.ErrorContains(t, err, "panic: cngcrypto: not in FIPS mode", args...) +} diff --git a/test/new-e2e/tests/fips-compliance/fixtures/e2e_fips_test.py b/test/new-e2e/tests/fips-compliance/fixtures/e2e_fips_test.py new file mode 100644 index 00000000000000..31841b6db8b751 --- /dev/null +++ b/test/new-e2e/tests/fips-compliance/fixtures/e2e_fips_test.py @@ -0,0 +1,21 @@ +import _hashlib +import win32api +from checks import AgentCheck + + +class FIPSModeCheck(AgentCheck): + def check(self, instance): + self.gauge('e2e.fips_mode', _hashlib.get_fips_mode()) + # _hashlib.get_fips_mode() only tests the fipsmodule.cnf enabled value + # it doesn't mean that FIPS mode is operating correctly, so we check + # that the FIPS provider DLL is loaded as well + self.gauge('e2e.fips_dll_loaded', _is_fips_dll_loaded()) + + +def _is_fips_dll_loaded(): + # the module is loaded on demand, import _hashlib is enough to load itf + try: + handle = win32api.GetModuleHandle("fips.dll") + except Exception: + handle = 0 + return handle != 0 diff --git a/test/new-e2e/tests/windows/common/powershell/command_builder.go b/test/new-e2e/tests/windows/common/powershell/command_builder.go index 9113ce42ce52a3..7486a8aee6675d 100644 --- a/test/new-e2e/tests/windows/common/powershell/command_builder.go +++ b/test/new-e2e/tests/windows/common/powershell/command_builder.go @@ -180,6 +180,22 @@ func (ps *powerShellCommandBuilder) UninstallWindowsDefender() *powerShellComman return ps } +// SetFIPSMode creates a command to enable or disable FIPS mode on the host +func (ps *powerShellCommandBuilder) SetFIPSMode(enabled bool) *powerShellCommandBuilder { + path := `HKLM:\SYSTEM\CurrentControlSet\Control\Lsa\FipsAlgorithmPolicy` + name := "Enabled" + var value string + if enabled { + value = "1" + } else { + value = "0" + } + typeName := "DWORD" + cmd := fmt.Sprintf("New-Item -Path '%s' -Force; Set-ItemProperty -Path '%s' -Name '%s' -Value '%s' -Type '%s'", path, path, name, value, typeName) + ps.cmds = append(ps.cmds, cmd) + return ps +} + // Execute compiles the list of PowerShell commands into one script and runs it on the given host func (ps *powerShellCommandBuilder) Execute(host *components.RemoteHost) (string, error) { return host.Execute(ps.Compile()) diff --git a/test/new-e2e/tests/windows/components/fipsmode/component.go b/test/new-e2e/tests/windows/components/fipsmode/component.go new file mode 100644 index 00000000000000..a04bb91521e05c --- /dev/null +++ b/test/new-e2e/tests/windows/components/fipsmode/component.go @@ -0,0 +1,57 @@ +// Unless explicitly stated otherwise all files in this repository are licensed +// under the Apache License Version 2.0. +// This product includes software developed at Datadog (https://www.datadoghq.com/). +// Copyright 2024-present Datadog, Inc. + +// Package fipsmode contains code to control the behavior of Windows FIPS mode in the E2E tests +package fipsmode + +import ( + "github.com/DataDog/datadog-agent/test/new-e2e/tests/windows/common/powershell" + "github.com/DataDog/test-infra-definitions/common" + "github.com/DataDog/test-infra-definitions/common/config" + "github.com/DataDog/test-infra-definitions/common/namer" + "github.com/DataDog/test-infra-definitions/components/command" + "github.com/DataDog/test-infra-definitions/components/remote" + "github.com/pulumi/pulumi/sdk/v3/go/pulumi" +) + +// Manager contains the resources to manage Windows FIPS mode +// +// https://learn.microsoft.com/en-us/previous-versions/windows/it-pro/windows-10/security/threat-protection/security-policy-settings/system-cryptography-use-fips-compliant-algorithms-for-encryption-hashing-and-signing +type Manager struct { + namer namer.Namer + host *remote.Host + + Resources []pulumi.Resource +} + +// New creates a new instance of the Windows FIPS mode component +func New(e *config.CommonEnvironment, host *remote.Host, options ...Option) (*Manager, error) { + params, err := common.ApplyOption(&Configuration{}, options) + if err != nil { + return nil, err + } + + manager := &Manager{ + namer: e.CommonNamer().WithPrefix("windows-fips-mode"), + host: host, + } + + if params.FIPSModeEnabled { + cmd, err := host.OS.Runner().Command(manager.namer.ResourceName("enable"), &command.Args{ + Create: pulumi.String(powershell.PsHost(). + SetFIPSMode(true). + Compile()), + Delete: pulumi.String(powershell.PsHost(). + SetFIPSMode(false). + Compile()), + }) + if err != nil { + return nil, err + } + manager.Resources = append(manager.Resources, cmd) + } + + return manager, nil +} diff --git a/test/new-e2e/tests/windows/components/fipsmode/params.go b/test/new-e2e/tests/windows/components/fipsmode/params.go new file mode 100644 index 00000000000000..7bad0a88895074 --- /dev/null +++ b/test/new-e2e/tests/windows/components/fipsmode/params.go @@ -0,0 +1,24 @@ +// Unless explicitly stated otherwise all files in this repository are licensed +// under the Apache License Version 2.0. +// This product includes software developed at Datadog (https://www.datadoghq.com/). +// Copyright 2024-present Datadog, Inc. + +package fipsmode + +// Configuration represents the Windows FIPS mode configuration +type Configuration struct { + FIPSModeEnabled bool +} + +// Option is an optional function parameter type for Configuration options +type Option = func(*Configuration) error + +// WithFIPSModeEnabled configures the FIPSMode component to enable FIPS mode on the Windows host +// +// https://learn.microsoft.com/en-us/previous-versions/windows/it-pro/windows-10/security/threat-protection/security-policy-settings/system-cryptography-use-fips-compliant-algorithms-for-encryption-hashing-and-signing +func WithFIPSModeEnabled() func(*Configuration) error { + return func(p *Configuration) error { + p.FIPSModeEnabled = true + return nil + } +} diff --git a/test/new-e2e/tests/windows/fips-test/fips_test.go b/test/new-e2e/tests/windows/fips-test/fips_test.go index afbda5cc51594b..9c0f9bfc12f0c6 100644 --- a/test/new-e2e/tests/windows/fips-test/fips_test.go +++ b/test/new-e2e/tests/windows/fips-test/fips_test.go @@ -10,26 +10,24 @@ import ( "fmt" "os" "path" - "path/filepath" "github.com/DataDog/datadog-agent/test/new-e2e/pkg/e2e" "github.com/DataDog/datadog-agent/test/new-e2e/pkg/environments" awsHostWindows "github.com/DataDog/datadog-agent/test/new-e2e/pkg/provisioners/aws/host/windows" - "github.com/DataDog/datadog-agent/test/new-e2e/pkg/utils/e2e/client" "github.com/DataDog/datadog-agent/test/new-e2e/tests/windows" windowsCommon "github.com/DataDog/datadog-agent/test/new-e2e/tests/windows/common" windowsAgent "github.com/DataDog/datadog-agent/test/new-e2e/tests/windows/common/agent" - "testing" - "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "testing" ) type fipsAgentSuite struct { windows.BaseAgentInstallerSuite[environments.WindowsHost] installPath string + configRoot string } func TestFIPSAgent(t *testing.T) { @@ -38,6 +36,15 @@ func TestFIPSAgent(t *testing.T) { e2e.Run(t, s, opts...) } +func TestFIPSAgentAltDir(t *testing.T) { + opts := []e2e.SuiteOption{e2e.WithProvisioner(awsHostWindows.ProvisionerNoAgentNoFakeIntake())} + s := &fipsAgentSuite{ + installPath: "C:\\altdir", + configRoot: "C:\\altconfroot", + } + e2e.Run(t, s, opts...) +} + func (s *fipsAgentSuite) SetupSuite() { // Default to using FIPS Agent package if _, set := windowsAgent.LookupFlavorFromEnv(); !set { @@ -53,67 +60,22 @@ func (s *fipsAgentSuite) SetupSuite() { require.NoError(s.T(), err) // Install Agent (With FIPS mode enabled) - _, err = s.InstallAgent(host, windowsAgent.WithPackage(s.AgentPackage)) + opts := []windowsAgent.InstallAgentOption{ + windowsAgent.WithPackage(s.AgentPackage), + } + if s.installPath != "" { + opts = append(opts, windowsAgent.WithProjectLocation(s.installPath)) + } + if s.configRoot != "" { + opts = append(opts, windowsAgent.WithApplicationDataDirectory(s.configRoot)) + } + _, err = s.InstallAgent(host, opts...) require.NoError(s.T(), err) s.installPath, err = windowsAgent.GetInstallPathFromRegistry(host) require.NoError(s.T(), err) -} - -func (s *fipsAgentSuite) TestWithSystemFIPSDisabled() { - host := s.Env().RemoteHost - windowsCommon.DisableFIPSMode(host) - - s.Run("version command", func() { - s.Run("gofips enabled", func() { - _, err := s.execAgentCommandWithFIPS("version") - assertErrorContainsFIPSPanic(s.T(), err, "agent should panic when GOFIPS=1 but system FIPS is disabled") - }) - - s.Run("gofips disabled", func() { - _, err := s.execAgentCommand("version") - require.NoError(s.T(), err) - }) - }) - - s.Run("status command", func() { - s.Run("gofips disabled", func() { - status, err := s.execAgentCommand("status") - require.NoError(s.T(), err) - assert.Contains(s.T(), status, "FIPS Mode: disabled") - }) - }) -} - -func (s *fipsAgentSuite) TestWithSystemFIPSEnabled() { - host := s.Env().RemoteHost - windowsCommon.EnableFIPSMode(host) - - s.Run("version command", func() { - s.Run("gofips enabled", func() { - _, err := s.execAgentCommandWithFIPS("version") - require.NoError(s.T(), err) - }) - - s.Run("gofips disabled", func() { - _, err := s.execAgentCommand("version") - require.NoError(s.T(), err) - }) - }) - - s.Run("status command", func() { - s.Run("gofips enabled", func() { - status, err := s.execAgentCommand("status") - require.NoError(s.T(), err) - assert.Contains(s.T(), status, "FIPS Mode: enabled") - }) - - s.Run("gofips disabled", func() { - status, err := s.execAgentCommand("status") - require.NoError(s.T(), err) - assert.Contains(s.T(), status, "FIPS Mode: enabled") - }) - }) + s.configRoot, err = windowsAgent.GetConfigRootFromRegistry(host) + require.NoError(s.T(), err) } func (s *fipsAgentSuite) TestFIPSProviderPresent() { @@ -122,6 +84,7 @@ func (s *fipsAgentSuite) TestFIPSProviderPresent() { require.True(s.T(), exists, "Agent install path should contain the FIPS provider but doesn't") } +// TestFIPSInstall tests that the MSI created a valid fipsmodule.cnf func (s *fipsAgentSuite) TestFIPSInstall() { host := s.Env().RemoteHost openssl := path.Join(s.installPath, "embedded3/bin/openssl.exe") @@ -132,31 +95,51 @@ func (s *fipsAgentSuite) TestFIPSInstall() { require.NoError(s.T(), err, "MSI should create valid fipsmodule.cnf") } -func (s *fipsAgentSuite) execAgentCommand(command string, options ...client.ExecuteOption) (string, error) { +// TestOpenSSLPaths tests that the MSI sets the OpenSSL paths in the registry +func (s *fipsAgentSuite) TestOpenSSLPaths() { host := s.Env().RemoteHost - require.NotEmpty(s.T(), s.installPath) - agentPath := filepath.Join(s.installPath, "bin", "agent.exe") - - cmd := fmt.Sprintf(`& "%s" %s`, agentPath, command) - return host.Execute(cmd, options...) -} - -func (s *fipsAgentSuite) execAgentCommandWithFIPS(command string) (string, error) { - // There isn't support for appending env vars to client.ExecuteOption, so - // this function doesn't accept any other options. - - // Setting GOFIPS=1 causes the Windows FIPS Agent to panic if the system is not in FIPS mode. - // This setting does NOT control whether the FIPS Agent uses FIPS-compliant crypto libraries, - // the System-level setting determines that. - // https://github.com/microsoft/go/tree/microsoft/main/eng/doc/fips#windows-fips-mode-cng - vars := client.EnvVar{ - "GOFIPS": "1", + // assert openssl winctx registry keys exist + // https://github.com/openssl/openssl/blob/master/NOTES-WINDOWS.md#installation-directories + expectedOpenSSLPaths := map[string]string{ + "OPENSSLDIR": fmt.Sprintf(`%sembedded3\ssl`, s.installPath), + "ENGINESDIR": fmt.Sprintf(`%sembedded3\lib\engines-3`, s.installPath), + "MODULESDIR": fmt.Sprintf(`%sembedded3\lib\ossl-modules`, s.installPath), + } + // TODO: How to configure the version of OpenSSL? + opensslVersion := "3.4" + keyPath := fmt.Sprintf(`HKLM:\SOFTWARE\Wow6432Node\OpenSSL-%s-datadog-fips-agent`, opensslVersion) + exists, err := windowsCommon.RegistryKeyExists(host, keyPath) + require.NoError(s.T(), err) + if assert.True(s.T(), exists, "%s should exist", keyPath) { + for name, expected := range expectedOpenSSLPaths { + // check value matches + value, err := windowsCommon.GetRegistryValue(host, keyPath, name) + if assert.NoError(s.T(), err, "Failed to get %s", name) { + assert.Equal(s.T(), expected, value, "Unexpected value for %s", name) + } + // ensure value exists as a directory + fileInfo, err := host.Lstat(value) + if assert.NoError(s.T(), err, "Path %s for %s does not exist", value, name) { + assert.True(s.T(), fileInfo.IsDir(), "Path %s for %s is not a directory", value, name) + } + } } - return s.execAgentCommand(command, client.WithEnvVariables(vars)) -} - -func assertErrorContainsFIPSPanic(t *testing.T, err error, args ...interface{}) bool { - return assert.ErrorContains(t, err, "panic: cngcrypto: not in FIPS mode", args...) + // assert that openssl uses the paths from the registry + // Example output: + // OpenSSL 3.3.2 3 Sep 2024 (Library: OpenSSL 3.3.2 3 Sep 2024) + // + // compiler: gcc -DOSSL_WINCTX=datadog-fips-agent + // OPENSSLDIR: "C:\Program Files\Datadog\Datadog Agent\embedded3\ssl" + // ENGINESDIR: "C:\Program Files\Datadog\Datadog Agent\embedded3\lib\engines-3" + // MODULESDIR: "C:\Program Files\Datadog\Datadog Agent\embedded3\lib\ossl-modules" + openssl := path.Join(s.installPath, `embedded3\bin\openssl.exe`) + cmd := fmt.Sprintf(`& "%s" version -a`, openssl) + out, err := host.Execute(cmd) + require.NoError(s.T(), err) + assert.Contains(s.T(), out, `-DOSSL_WINCTX=datadog-fips-agent`, "Expected -DOSSL_WINCTX=datadog-fips-agent in openssl.exe output") + for name, expected := range expectedOpenSSLPaths { + assert.Contains(s.T(), out, fmt.Sprintf(`%s: "%s"`, name, expected), "Expected %s to be %s", name, expected) + } } diff --git a/tools/windows/DatadogAgentInstaller/WixSetup/Datadog Agent/AgentFlavor.cs b/tools/windows/DatadogAgentInstaller/WixSetup/Datadog Agent/AgentFlavor.cs index 62022d0e7542a9..90127bfa21e755 100644 --- a/tools/windows/DatadogAgentInstaller/WixSetup/Datadog Agent/AgentFlavor.cs +++ b/tools/windows/DatadogAgentInstaller/WixSetup/Datadog Agent/AgentFlavor.cs @@ -41,6 +41,8 @@ internal interface IAgentFlavor Guid UpgradeCode { get; } string ProductDescription { get; } string PackageOutFileName { get; } + // https://github.com/openssl/openssl/blob/master/NOTES-WINDOWS.md#installation-directories + string OpenSSLWinCtx { get; } } internal class FIPSAgent : IAgentFlavor @@ -59,6 +61,7 @@ public FIPSAgent(AgentVersion agentVersion) public Guid UpgradeCode => new("de421174-9615-4fe9-b8a8-2b3f123bdc4f"); public string ProductDescription => $"Datadog FIPS Agent {_agentVersion.PackageVersion}"; public string PackageOutFileName => $"datadog-fips-agent-{_agentNameSuffix}{_agentVersion.PackageVersion}-1-x86_64"; + public string OpenSSLWinCtx => "datadog-fips-agent"; } internal class BaseAgent : IAgentFlavor @@ -77,5 +80,6 @@ public BaseAgent(AgentVersion agentVersion) public Guid UpgradeCode => new("0c50421b-aefb-4f15-a809-7af256d608a5"); public string ProductDescription => $"Datadog Agent {_agentVersion.PackageVersion}"; public string PackageOutFileName => $"datadog-agent-{_agentNameSuffix}{_agentVersion.PackageVersion}-1-x86_64"; + public string OpenSSLWinCtx => "datadog-agent"; } } diff --git a/tools/windows/DatadogAgentInstaller/WixSetup/Datadog Agent/AgentInstaller.cs b/tools/windows/DatadogAgentInstaller/WixSetup/Datadog Agent/AgentInstaller.cs index fbd0a4ee7c6bd3..b50c44e5221137 100644 --- a/tools/windows/DatadogAgentInstaller/WixSetup/Datadog Agent/AgentInstaller.cs +++ b/tools/windows/DatadogAgentInstaller/WixSetup/Datadog Agent/AgentInstaller.cs @@ -145,6 +145,34 @@ public Project Configure() Win64 = true } ); + var agentOpenSSLVersion = Environment.GetEnvironmentVariable("AGENT_OPENSSL_VERSION"); + if (!string.IsNullOrEmpty(agentOpenSSLVersion)) + { + // Since OpenSSL 3.4, the install paths can be retrieved from the registry instead of being hardcoded at build time. + // This is important because the Agent install path isn't known at build time. + // At time of writing, this is only relevant for FIPS Agent, we don't configure OpenSSL in regular Agent builds. + // https://github.com/openssl/openssl/blob/master/NOTES-WINDOWS.md#installation-directories + if (agentOpenSSLVersion.Split('.').Length != 2) + { + throw new InvalidDataException("AGENT_OPENSSL_VERSION must be in the format MAJOR.MINOR"); + } + var winctx = $"OpenSSL-{agentOpenSSLVersion}-{_agentFlavor.OpenSSLWinCtx}"; + // Store key name as property for easy reference, and to use in InstallState action to write the relevant keys + // at install time, once the install path is known. + project.AddProperty(new Property("AgentOpenSSLWinCtx", winctx)); + // Store the root key in WiX so that we can depend on deleting/restoring the keys/values during rollback + project.AddRegKey(new RegKey(_agentFeatures.MainApplication, + RegistryHive.LocalMachine, $@"Software\WOW6432Node\{winctx}", + // Must set KeyPath=yes to ensure WiX# doesn't automatically try to use the parent Directory as the KeyPath, + // which can cause the directory to be added to the CreateFolder table. + // We are able to assume PROJECTLOCATION includes a trailing backslash b/c the built-in WriteRegistryValues + // action runs after file costing. + new RegValue("OPENSSLDIR", "[PROJECTLOCATION]embedded3\\ssl") { Win64 = true, AttributesDefinition = "KeyPath=yes" }, + new RegValue("ENGINESDIR", "[PROJECTLOCATION]embedded3\\lib\\engines-3") { Win64 = true, AttributesDefinition = "KeyPath=yes" }, + new RegValue("MODULESDIR", "[PROJECTLOCATION]embedded3\\lib\\ossl-modules") { Win64 = true, AttributesDefinition = "KeyPath=yes" } + ) + ); + } // Always generate a new GUID otherwise WixSharp will generate one based on // the version