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
4 changes: 2 additions & 2 deletions .gitattributes
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
*.tsx text eol=lf
*.json text eol=lf
*.ps1 text eol=lf
*.sh text eol=lf
*.sh text eol=lf
*.sln text eol=crlf
*.md text eol=lf
*.mustache text eol=lf
Expand All @@ -25,4 +25,4 @@ devolutions-gateway/openapi/ts-angular-client/api/** linguist-generated merge=bi
devolutions-gateway/openapi/ts-angular-client/model/** linguist-generated merge=binary

# Sample assets produce huge LoC counts; exclude them from language statistics.
devolutions-agent/src/broker/assets/** linguist-generated
crates/now-package-broker/src/assets/** linguist-generated
19 changes: 18 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -1197,7 +1197,24 @@ jobs:
- name: Prepare runner
run: |
rustup toolchain install nightly
choco install pstools --yes

# The pstools Chocolatey package is unreliable: it downloads PSTools.zip from
# Microsoft at install time, and its pinned checksum breaks whenever Microsoft
# updates the zip. Download it directly and verify against our own checksum
# instead; bump the hash deliberately when Microsoft publishes a new PSTools.
- name: Install PsExec
shell: pwsh
run: |
$expectedHash = '4F49964CC9CBAC2B5D87BDC8F9526012E9C4B243D8B7D0C0BB51F254A721CA2E'
$zipPath = Join-Path $env:RUNNER_TEMP 'PSTools.zip'
$toolsDir = Join-Path $env:RUNNER_TEMP 'PSTools'
Invoke-WebRequest -Uri 'https://download.sysinternals.com/files/PSTools.zip' -OutFile $zipPath
$actualHash = (Get-FileHash -Path $zipPath -Algorithm SHA256).Hash
if ($actualHash -ne $expectedHash) {
throw "PSTools.zip checksum mismatch: expected $expectedHash, got $actualHash"
}
Expand-Archive -Path $zipPath -DestinationPath $toolsDir
Add-Content -Path $env:GITHUB_PATH -Value $toolsDir

# The Docker service for Windows containers may not be started yet at this point.
# Starting it explicitly avoids flaky failures when the daemon is not ready.
Expand Down
18 changes: 17 additions & 1 deletion .github/workflows/publish-libraries.yml
Original file line number Diff line number Diff line change
Expand Up @@ -240,7 +240,23 @@ jobs:
shell: pwsh

- name: Update Artifactory Cache
run: gh workflow run update-artifactory-cache.yml --repo Devolutions/scheduled-tasks --field package_name="gateway-client"
run: |
Set-PSDebug -Trace 1

$Files = Get-ChildItem -Recurse npm-packages/*.tgz

foreach ($File in $Files) {
$Manifest = (tar -xzOf "$File" package/package.json | Out-String) | ConvertFrom-Json
$PackageName = $Manifest.name -Replace '^@devolutions/', ''

Write-Host "Updating Artifactory cache for $($Manifest.name)..."
gh workflow run update-artifactory-cache.yml --repo Devolutions/scheduled-tasks --field package_name="$PackageName"

if ($LastExitCode -Ne 0) {
throw "Failed to trigger the Artifactory cache update for $($Manifest.name)"
}
}
shell: pwsh
env:
GH_TOKEN: ${{ secrets.DEVOLUTIONSBOT_WRITE_TOKEN }}

Expand Down
70 changes: 45 additions & 25 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions crates/devolutions-agent-shared/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,10 @@ thiserror = "2"
tracing = "0.1"

[target.'cfg(windows)'.dependencies]
anyhow = "1"
hex = "0.4"
win-api-wrappers = { path = "../win-api-wrappers" }
windows = { version = "0.61", features = ["Win32_Foundation", "Win32_Security_Cryptography"] }
windows-registry = "0.5"
# Required for `windows-registry` error type. (`Error` is not reexported from `windows-registry`).
windows-result = "0.3"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,13 @@ use anyhow::{Context as _, bail};
use win_api_wrappers::security::crypt::{AuthenticodeSignatureStatus, authenticode_status};

/// List of allowed thumbprints for Devolutions code signing certificates.
pub(crate) const DEVOLUTIONS_CERT_THUMBPRINTS: &[&str] = &[
pub const DEVOLUTIONS_CERT_THUMBPRINTS: &[&str] = &[
"3f5202a9432d54293bdfe6f7e46adb0a6f8b3ba6",
"8db5a43bb8afe4d2ffb92da9007d8997a4cc4e13",
"50f753333811ff11f1920274afde3ffd4468b210",
];

pub(crate) fn certificate_sha1_thumbprint(cert_der: &[u8]) -> anyhow::Result<[u8; 20]> {
pub fn certificate_sha1_thumbprint(cert_der: &[u8]) -> anyhow::Result<[u8; 20]> {
use windows::Win32::Security::Cryptography::{CALG_SHA1, CryptHashCertificate};

let mut thumbprint = [0u8; 20];
Expand All @@ -39,7 +39,7 @@ pub(crate) fn certificate_sha1_thumbprint(cert_der: &[u8]) -> anyhow::Result<[u8
Ok(thumbprint)
}

pub(crate) fn is_devolutions_certificate_thumbprint(calculated_thumbprint: &[u8; 20]) -> bool {
pub fn is_devolutions_certificate_thumbprint(calculated_thumbprint: &[u8; 20]) -> bool {
DEVOLUTIONS_CERT_THUMBPRINTS.iter().any(|thumbprint| {
let mut thumbprint_bytes = [0u8; 20];
hex::decode_to_slice(thumbprint, &mut thumbprint_bytes)
Expand All @@ -49,32 +49,32 @@ pub(crate) fn is_devolutions_certificate_thumbprint(calculated_thumbprint: &[u8;
})
}

pub(crate) fn validate_devolutions_authenticode_signature(path: &Path) -> anyhow::Result<String> {
pub fn validate_devolutions_authenticode_signature(path: &Path) -> anyhow::Result<String> {
let wintrust_result = authenticode_status(path).with_context(|| {
format!(
"failed to read authenticode signature for client executable '{}'",
"failed to read authenticode signature for executable '{}'",
path.display()
)
})?;

if !matches!(wintrust_result.status, AuthenticodeSignatureStatus::Valid) {
bail!("client executable signature is not valid: {:?}", wintrust_result.status);
bail!("executable signature is not valid: {:?}", wintrust_result.status);
}

let signer = wintrust_result
.provider
.as_ref()
.and_then(|provider| provider.signers.first())
.context("client executable signature has no signer")?;
.context("executable signature has no signer")?;
let signing_cert = signer
.cert_chain
.first()
.context("client executable signature has no signing certificate")?;
.context("executable signature has no signing certificate")?;

let thumbprint = certificate_sha1_thumbprint(&signing_cert.cert.encoded)?;
if !is_devolutions_certificate_thumbprint(&thumbprint) {
bail!(
"client executable is signed with an unexpected certificate thumbprint: {}",
"executable is signed with an unexpected certificate thumbprint: {}",
hex::encode(thumbprint)
);
}
Expand Down
1 change: 1 addition & 0 deletions crates/devolutions-agent-shared/src/windows/mod.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
mod reversed_hex_uuid;

pub mod code_signing;
pub mod registry;

use uuid::{Uuid, uuid};
Expand Down
71 changes: 71 additions & 0 deletions crates/now-package-broker/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
[package]
name = "now-package-broker"
version = "0.0.0"
edition = "2024"
license = "MIT/Apache-2.0"
authors = ["Devolutions Inc. <infos@devolutions.net>"]
description = "Package broker for the Devolutions Agent"
publish = false

[features]
default = []
# Development-only feature allowing broker client signature validation to be skipped.
# Must never be enabled for shipped builds: without it, broker client signature validation is
# unconditionally enforced regardless of the configuration file contents.
dev-skip-broker-signature = []

[lints]
workspace = true

# The broker is only functional on Windows; all dependencies are Windows-only
# so the crate compiles to an empty library on other platforms.
[target.'cfg(windows)'.dependencies]
anyhow = "1"
async-trait = "0.1"
axum = { version = "0.8", default-features = false, features = ["http1", "json", "tokio", "original-uri", "matched-path"] }
chrono = { version = "0.4", features = ["serde"] }
devolutions-agent-shared = { path = "../devolutions-agent-shared" }
devolutions-gateway-task = { path = "../devolutions-gateway-task" }
hex = "0.4"
hyper = { version = "1", features = ["http1", "server"] }
hyper-util = { version = "0.1", features = ["tokio", "server", "server-auto", "service"] }
notify = { version = "7", default-features = false }
now-policy = "0.2"
now-policy-api = { version = "0.3", features = ["policy-compat"] }
now-policy-server-template = { version = "0.3", features = ["policy-compat"] }
parking_lot = "0.12"
regex = "1"
semver = "1"
serde_json = "1"
sha2 = "0.10"
tokio = { version = "1.52", features = ["net", "io-util", "rt", "macros", "parking_lot", "fs", "sync", "time"] }
tokio-util = "0.7"
tower-service = "0.3"
tracing = "0.1"
uuid = { version = "1.23", features = ["v4"] }
widestring = "1.2"
win-api-wrappers = { path = "../win-api-wrappers" }

[target.'cfg(windows)'.dependencies.windows]
version = "0.61"
features = [
"Win32_Foundation",
"Win32_Security",
"Win32_Security_Authorization",
"Win32_Storage_FileSystem",
"Win32_System_ApplicationInstallationAndServicing",
"Win32_System_Com",
"Win32_System_Console",
"Win32_System_IO",
"Win32_System_Ioctl",
"Win32_System_Pipes",
"Win32_System_Threading",
"Win32_UI_Shell",
"Win32_UI_WindowsAndMessaging",
]

[target.'cfg(windows)'.dev-dependencies]
serde = { version = "1", features = ["derive"] }
serde_yaml = "0.9"
tempfile = "3"
tokio = { version = "1.52", features = ["rt-multi-thread"] }
Loading
Loading