Skip to content
Open
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
30 changes: 30 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -878,6 +878,9 @@ jobs:
$DAgentSessionExecutable = Join-Path $TargetOutputPath "DevolutionsSession.exe"
echo "dagent-session-executable=$DAgentSessionExecutable" >> $Env:GITHUB_OUTPUT

$DAgentPolicyConsentHelper = Join-Path $TargetOutputPath "DevolutionsAgentPolicyConsent.exe"
echo "dagent-policy-consent-helper=$DAgentPolicyConsentHelper" >> $Env:GITHUB_OUTPUT

$DAgentUpdaterExecutable = Join-Path $TargetOutputPath "DevolutionsAgentUpdater.exe"
echo "dagent-updater-executable=$DAgentUpdaterExecutable" >> $Env:GITHUB_OUTPUT
}
Expand Down Expand Up @@ -1043,6 +1046,28 @@ jobs:
DAGENT_EXECUTABLE: ${{ steps.load-variables.outputs.dagent-executable }}
TARGET_OUTPUT_PATH: ${{ steps.load-variables.outputs.target-output-path }}

- name: Build NativeAOT policy consent helper
if: ${{ matrix.os == 'windows' }}
run: |
$Rid = "win-${{ matrix.arch }}"
$Output = Split-Path -Parent '${{ steps.load-variables.outputs.dagent-policy-consent-helper }}'
dotnet publish package/AgentPolicyConsent/DevolutionsAgentPolicyConsent.csproj `
--configuration Release `
--runtime $Rid `
--output $Output `
-p:Version=${{ needs.preflight.outputs.version }}
if ($LASTEXITCODE -ne 0) {
exit $LASTEXITCODE
}
$Helper = '${{ steps.load-variables.outputs.dagent-policy-consent-helper }}'
if (-Not (Test-Path -LiteralPath $Helper -PathType Leaf)) {
throw "NativeAOT policy consent helper was not produced"
}
if ((Get-Item -LiteralPath $Helper).Length -gt 8MB) {
throw "NativeAOT policy consent helper exceeds 8 MiB"
}
shell: pwsh

- name: Package
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository }}
run: |
Expand All @@ -1056,6 +1081,7 @@ jobs:
$Env:DAGENT_PEDM_SHELL_EXT_DLL = "${{ steps.load-variables.outputs.dagent-pedm-shell-ext-dll }}"
$Env:DAGENT_PEDM_SHELL_EXT_MSIX = "${{ steps.load-variables.outputs.dagent-pedm-shell-ext-msix }}"
$Env:DAGENT_SESSION_EXECUTABLE = "${{ steps.load-variables.outputs.dagent-session-executable }}"
$Env:DAGENT_POLICY_CONSENT_HELPER = "${{ steps.load-variables.outputs.dagent-policy-consent-helper }}"
$Env:DAGENT_TUN2SOCKS_EXE = "${{ steps.tun2socks.outputs.tun2socks-executable-path }}"
$Env:DAGENT_WINTUN_DLL = "${{ steps.tun2socks.outputs.wintun-library-path }}"
$Env:DAGENT_MULTI_PWSH_EXECUTABLE = "${{ steps.multi-pwsh.outputs.executable-path }}"
Expand Down Expand Up @@ -1177,6 +1203,10 @@ jobs:
run: dotnet test package/AgentWindowsManaged.Tests/DevolutionsAgent.Installer.Tests.csproj
shell: pwsh

- name: Policy consent helper tests
run: dotnet test package/AgentPolicyConsent.Tests/DevolutionsAgentPolicyConsent.Tests.csproj -c Release
shell: pwsh


winapi-sanitizer-tests:
name: Windows API sanitizer tests
Expand Down
7 changes: 5 additions & 2 deletions .github/workflows/package.yml
Original file line number Diff line number Diff line change
Expand Up @@ -322,7 +322,7 @@ jobs:
run: |
$IncludePattern = @(switch ('${{ matrix.project }}') {
'devolutions-gateway' { @('DevolutionsGateway.exe') }
'devolutions-agent' { @('DevolutionsAgent.exe', 'DevolutionsAgentUpdater.exe', 'DevolutionsPedmShellExt.dll', 'DevolutionsPedmShellExt.msix', 'DevolutionsDesktopAgent.exe') }
'devolutions-agent' { @('DevolutionsAgent.exe', 'DevolutionsAgentUpdater.exe', 'DevolutionsAgentPolicyConsent.exe', 'DevolutionsPedmShellExt.dll', 'DevolutionsPedmShellExt.msix', 'DevolutionsDesktopAgent.exe') }
'jetsocat' { @('jetsocat.exe', 'jetsocat') }
})
$ExcludePattern = "*.pdb"
Expand Down Expand Up @@ -495,6 +495,7 @@ jobs:
$Env:DAGENT_PEDM_SHELL_EXT_DLL = Get-ChildItem -Path $ArchRoot -Filter 'DevolutionsPedmShellExt.dll' -File | Select-Object -First 1
$Env:DAGENT_PEDM_SHELL_EXT_MSIX = Get-ChildItem -Path $ArchRoot -Filter 'DevolutionsPedmShellExt.msix' -File | Select-Object -First 1
$Env:DAGENT_SESSION_EXECUTABLE = Get-ChildItem -Path $ArchRoot -Filter 'DevolutionsSession.exe' -File | Select-Object -First 1
$Env:DAGENT_POLICY_CONSENT_HELPER = Get-ChildItem -Path $ArchRoot -Filter 'DevolutionsAgentPolicyConsent.exe' -File | Select-Object -First 1
$Env:DAGENT_TUN2SOCKS_EXE = Join-Path $ArchRoot 'tun2socks.exe'
$Env:DAGENT_WINTUN_DLL = Join-Path $ArchRoot 'wintun.dll'
$MultiPwshDirectory = Join-Path $Env:RUNNER_TEMP 'multi-pwsh' 'windows' $Arch
Expand All @@ -508,6 +509,7 @@ jobs:
Write-Host "DAGENT_PEDM_SHELL_EXT_DLL = ${Env:DAGENT_PEDM_SHELL_EXT_DLL}"
Write-Host "DAGENT_PEDM_SHELL_EXT_MSIX = ${Env:DAGENT_PEDM_SHELL_EXT_MSIX}"
Write-Host "DAGENT_SESSION_EXECUTABLE = ${Env:DAGENT_SESSION_EXECUTABLE}"
Write-Host "DAGENT_POLICY_CONSENT_HELPER = ${Env:DAGENT_POLICY_CONSENT_HELPER}"
Write-Host "DAGENT_TUN2SOCKS_EXE = ${Env:DAGENT_TUN2SOCKS_EXE}"
Write-Host "DAGENT_WINTUN_DLL = ${Env:DAGENT_WINTUN_DLL}"
Write-Host "DAGENT_MULTI_PWSH_EXECUTABLE = ${Env:DAGENT_MULTI_PWSH_EXECUTABLE}"
Expand All @@ -534,7 +536,8 @@ jobs:
@((Join-Path $ArchRoot DesktopAgent),
(Get-ChildItem -Path $ArchRoot -Filter 'DevolutionsPedmShellExt.dll' | Select-Object -First 1),
(Get-ChildItem -Path $ArchRoot -Filter 'DevolutionsPedmShellExt.msix' | Select-Object -First 1),
(Get-ChildItem -Path $ArchRoot -Filter 'DevolutionsSession.exe' | Select-Object -First 1)) | ForEach-Object {
(Get-ChildItem -Path $ArchRoot -Filter 'DevolutionsSession.exe' | Select-Object -First 1),
(Get-ChildItem -Path $ArchRoot -Filter 'DevolutionsAgentPolicyConsent.exe' | Select-Object -First 1)) | ForEach-Object {
Remove-Item $_ -Recurse -ErrorAction SilentlyContinue | Out-Null
}
}
Expand Down
10 changes: 9 additions & 1 deletion ci/package-agent-windows.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ param(
[parameter(Mandatory = $true)]
[string] $SessionExe,
[parameter(Mandatory = $true)]
[string] $PolicyConsentHelper,
[parameter(Mandatory = $true)]
[ValidateSet('x64', 'arm64')]
[string] $Architecture,
[string] $Outfile
Expand Down Expand Up @@ -98,6 +100,9 @@ function New-AgentMsi() {
# The path to the devolutions-session.exe file.
[string] $SessionExe,
[parameter(Mandatory = $true)]
# The path to the signed DevolutionsAgentPolicyConsent.exe file.
[string] $PolicyConsentHelper,
[parameter(Mandatory = $true)]
[ValidateSet('x64', 'arm64')]
# Architecture: x64 or arm64
[string] $Architecture,
Expand All @@ -120,6 +125,7 @@ function New-AgentMsi() {
$PedmDll = Convert-Path -Path $PedmDll
$PedmMsix = Convert-Path -Path $PedmMsix
$SessionExe = Convert-Path -Path $SessionExe
$PolicyConsentHelper = Convert-Path -Path $PolicyConsentHelper
if ($Outfile) {
$Outfile = Convert-Path -Path $Outfile
}
Expand All @@ -137,6 +143,7 @@ function New-AgentMsi() {
$myUpdaterExe = Set-FileNameAndCopy -Path $UpdaterExe -NewName 'DevolutionsAgentUpdater.exe'
# The session is a service that gets launched on demand.
$mySessionExe = Set-FileNameAndCopy -Path $SessionExe -NewName 'DevolutionsSession.exe'
$myPolicyConsentHelper = Set-FileNameAndCopy -Path $PolicyConsentHelper -NewName 'DevolutionsAgentPolicyConsent.exe'

Write-Output "$repoDir\dotnet\DesktopAgent\bin\Release\net48\DevolutionsDesktopAgent.exe"

Expand All @@ -145,6 +152,7 @@ function New-AgentMsi() {
Set-EnvVarPath 'DAGENT_PEDM_SHELL_EXT_DLL' $myPedmDll
Set-EnvVarPath 'DAGENT_PEDM_SHELL_EXT_MSIX' $myPedmMsix
Set-EnvVarPath 'DAGENT_SESSION_EXECUTABLE' $mySessionExe
Set-EnvVarPath 'DAGENT_POLICY_CONSENT_HELPER' $myPolicyConsentHelper

# The actual DevolutionsDesktopAgent.exe will be `\dotnet\DesktopAgent\bin\Release\net48\DevolutionsDesktopAgent.exe`.
# After install, the contents of `net48` will be copied to `C:\Program Files\Devolutions\Agent\desktop\`.
Expand Down Expand Up @@ -184,4 +192,4 @@ function New-AgentMsi() {
Pop-Location
}

New-AgentMsi -Generate:($Generate.IsPresent) -Exe $Exe -UpdaterExe $UpdaterExe -PedmDll $PedmDll -PedmMsix $PedmMsix -SessionExe $SessionExe -Architecture $Architecture -Outfile $Outfile
New-AgentMsi -Generate:($Generate.IsPresent) -Exe $Exe -UpdaterExe $UpdaterExe -PedmDll $PedmDll -PedmMsix $PedmMsix -SessionExe $SessionExe -PolicyConsentHelper $PolicyConsentHelper -Architecture $Architecture -Outfile $Outfile
83 changes: 83 additions & 0 deletions crates/now-package-broker/src/auth.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ use windows::Win32::System::Threading::{
use crate::policy_security::RetainedExecutableSecurity;

const PROCESS_SYNCHRONIZE: PROCESS_ACCESS_RIGHTS = PROCESS_ACCESS_RIGHTS(0x0010_0000);
const POLICY_CONSENT_HELPER_NAME: &str = "DevolutionsAgentPolicyConsent.exe";
const PROCESS_IDENTITY_ACCESS: PROCESS_ACCESS_RIGHTS = PROCESS_ACCESS_RIGHTS(
PROCESS_QUERY_INFORMATION.0 | PROCESS_QUERY_LIMITED_INFORMATION.0 | PROCESS_VM_READ.0 | PROCESS_SYNCHRONIZE.0,
);
Expand Down Expand Up @@ -289,6 +290,20 @@ impl PipeClient {
Ok(())
}

pub(crate) fn validate_policy_write(&self, skip_signature_validation: bool) -> anyhow::Result<()> {
self.validate_connection(skip_signature_validation)?;
// Dev builds cannot enforce helper identity when signature validation is explicitly disabled.
if signature_validation_skipped(skip_signature_validation) {
return Ok(());
}
let agent = std::env::current_exe().context("failed to query Agent executable path")?;
let executable_file = self
.executable_file
.as_deref()
.context("policy consent helper executable handle is not retained")?;
Self::validate_policy_consent_helper_path(&self.executable_path, executable_file, &agent)
}

fn validate_process_instance(&self) -> anyhow::Result<()> {
let Some(process) = &self.process else {
return Ok(());
Expand All @@ -304,6 +319,29 @@ impl PipeClient {
)
}

fn validate_policy_consent_helper_path(client: &Path, client_file: &File, agent: &Path) -> anyhow::Result<()> {
if !client
.file_name()
.is_some_and(|name| name.eq_ignore_ascii_case(POLICY_CONSENT_HELPER_NAME))
{
bail!("policy replacement requires the Agent policy consent helper");
}
let expected = agent
.parent()
.context("Agent executable has no installation directory")?
.join(POLICY_CONSENT_HELPER_NAME);
if !crate::policy_security::windows_paths_equal(client, &expected) {
bail!("policy consent helper is not the installed Agent helper path");
}
let expected_id = file_id(&expected).context("failed to query installed policy consent helper identity")?;
let retained_id =
file_id_from_handle(client_file).context("failed to query retained policy consent helper identity")?;
if !same_file(&expected_id, &retained_id) {
bail!("policy consent helper does not match the installed helper");
}
Ok(())
}

/// Validate that the request's `effective_user` denotes the authenticated pipe client user.
///
/// The name is resolved to a SID and compared against the SID captured at connect,
Expand Down Expand Up @@ -539,6 +577,51 @@ mod tests {
.expect_err("a recycled PID with a different creation time must be rejected");
}

#[test]
fn policy_consent_helper_requires_exact_agent_sibling_path() {
let current_executable = std::env::current_exe().expect("current executable");
let current_file = open_executable_file(&current_executable).expect("open current executable");
let agent = Path::new(r"C:\Program Files\Devolutions\Agent\DevolutionsAgent.exe");
assert!(
PipeClient::validate_policy_consent_helper_path(
Path::new(r"C:\Program Files\Devolutions\Agent\DevolutionsAgentPolicyConsent.exe"),
&current_file,
agent,
)
.is_err(),
"path text alone must not authorize a different retained image"
);
assert!(
PipeClient::validate_policy_consent_helper_path(
Path::new(r"C:\Users\Alice\DevolutionsAgentPolicyConsent.exe"),
&current_file,
agent,
)
.is_err()
);
assert!(
PipeClient::validate_policy_consent_helper_path(
Path::new(r"C:\Users\Alice\UniGetUI.exe"),
&current_file,
agent,
)
.is_err()
);
}

#[test]
fn policy_consent_helper_accepts_exact_retained_sibling() {
let temp = tempfile::tempdir().expect("temp directory");
let agent = temp.path().join("DevolutionsAgent.exe");
let helper = temp.path().join(POLICY_CONSENT_HELPER_NAME);
std::fs::write(&agent, b"agent path anchor").expect("write Agent path anchor");
std::fs::copy(std::env::current_exe().expect("current executable"), &helper).expect("copy helper fixture");
let retained = open_executable_file(&helper).expect("retain helper fixture");

PipeClient::validate_policy_consent_helper_path(&helper, &retained, &agent)
.expect("exact retained sibling must be accepted");
}

#[test]
fn exited_process_cannot_supply_executable_identity() {
let mut child = std::process::Command::new("powershell.exe")
Expand Down
7 changes: 6 additions & 1 deletion crates/now-package-broker/src/server/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,12 @@ async fn authenticate_policy_management(
| (&Method::PUT, "/v1/policy")
);
if protected {
if let Err(error) = client.validate_connection(state.skip_signature_validation) {
let authentication = if matches!((request.method(), request.uri().path()), (&Method::PUT, "/v1/policy")) {
client.validate_policy_write(state.skip_signature_validation)
} else {
client.validate_connection(state.skip_signature_validation)
};
if let Err(error) = authentication {
if let Some(audit) = write_audit {
audit.denied(crate::audit::DenialReason::AuthenticationFailed);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0-windows</TargetFramework>
<RuntimeIdentifier>win-x64</RuntimeIdentifier>
<SelfContained>true</SelfContained>
<IsPackable>false</IsPackable>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="18.0.1" />
<PackageReference Include="xunit.v3" Version="3.2.2" />
<PackageReference Include="xunit.runner.visualstudio" Version="4.0.0">
<PrivateAssets>all</PrivateAssets>
</PackageReference>
</ItemGroup>
<ItemGroup>
<ProjectReference
Include="..\AgentPolicyConsent\DevolutionsAgentPolicyConsent.csproj"
AdditionalProperties="PublishAot=false;SelfContained=false" />
</ItemGroup>
</Project>
Loading
Loading