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
14 changes: 14 additions & 0 deletions .github/workflows/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,20 @@ The workflow will display a notice if `run` was not built from the main branch.
- The `Codesign` job uses a matrix to code-sign and repackage in parallel. It's important to ensure that individual matrix jobs do not upload the same artifacts; otherwise the result may be unexpected. For example: each platform downloads the `jetsocat` artifact, which contains builds for several operating systems. The Windows job must only sign and upload the Windows builds. If it were allowed to upload the macOS builds, they will be unsigned and *may* overwrite the signed builds uploaded by the macOS job.
- The workflow makes a checkout of the repository at the commit that `run` was built from. This ensures consistency with the [CI](#ci) workflow when repackaging. Remember that tools invoked by the workflow (e.g. [tlk.ps1](../../ci/tlk.ps1)) will be from that commit too.

For macOS, make these GitHub Actions secrets available to the `publish-prod` environment:

| Secret | Purpose |
| --- | --- |
| `APPLE_APP_DEV_ID_APP_CERTIFICATE` | Developer ID Application certificate |
| `APPLE_APP_DEV_ID_APP_CERTIFICATE_PASSWORD` | Application certificate export password |
| `APPLE_DEV_ID_INSTALLER_CERTIFICATE` | Developer ID Installer certificate for the Agent package |
| `APPLE_DEV_ID_INSTALLER_CERTIFICATE_PASSWORD` | Installer certificate export password |
| `APPLE_BOT_PASSWORD` | App-specific Apple ID password for notarization |

Both certificates must be base64-encoded PKCS #12 bundles containing their private keys.
The Agent package requires a separate Developer ID Installer certificate.
Production packaging fails if the Installer certificate or notarization password is missing.

##### TODO

- `jetsocat` builds for macOS are signed, but should be notarized as well
Expand Down
24 changes: 22 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -65,8 +65,8 @@ jobs:

$Jobs = @()

$Platforms | ForEach-Object {
$Runner = switch ($_) { 'windows' { 'windows-2022' } 'linux' { 'ubuntu-22.04' } }
$Platforms + 'macos' | ForEach-Object {
$Runner = switch ($_) { 'windows' { 'windows-2022' } 'macos' { 'macos-15' } 'linux' { 'ubuntu-22.04' } }
foreach ($Arch in $Archs) {
$Jobs += @{
arch = $Arch
Expand Down Expand Up @@ -1136,6 +1136,17 @@ jobs:
with:
ref: ${{ needs.preflight.outputs.ref }}

# See the jetsocat job: the macos-15 image's ~/.cargo/bin shims are broken.
- name: Repair Rust shims (macOS)
if: ${{ matrix.os == 'macos' }}
run: |
set -eux
rm -rf "$HOME/.cargo/bin"
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs \
| bash -s -- -y --default-toolchain none --no-modify-path --profile minimal
echo "$HOME/.cargo/bin" >> $GITHUB_PATH
shell: bash

- name: Setup Rust cache
uses: ./.github/actions/setup-rust-cache
with:
Expand Down Expand Up @@ -1280,6 +1291,10 @@ jobs:
rustup target add aarch64-pc-windows-msvc
shell: pwsh

- name: Configure macOS (intel) runner
if: ${{ matrix.os == 'macos' && matrix.arch == 'x64' }}
run: rustup target add x86_64-apple-darwin

- name: Add msbuild to PATH
if: ${{ matrix.os == 'windows' }}
uses: microsoft/setup-msbuild@v3
Expand Down Expand Up @@ -1376,6 +1391,11 @@ jobs:
DAGENT_EXECUTABLE: ${{ steps.load-variables.outputs.dagent-executable }}
TARGET_OUTPUT_PATH: ${{ steps.load-variables.outputs.target-output-path }}

# The macos-15 runner is arm64, so it can only install the arm64 package.
- name: Test macOS package installation
if: ${{ matrix.os == 'macos' && matrix.arch == 'arm64' && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) }}
run: ./package/AgentMacOS/Test-Package.sh "${{ steps.load-variables.outputs.target-output-path }}/devolutions-agent.pkg"
shell: bash

- name: Clean up architecture-agnostic files before upload
run: |
Expand Down
101 changes: 85 additions & 16 deletions .github/workflows/package.yml
Original file line number Diff line number Diff line change
Expand Up @@ -193,8 +193,6 @@ jobs:
exclude:
- project: devolutions-gateway
os: macos
- project: devolutions-agent
os: macos

steps:
- name: Checkout ${{ github.repository }}
Expand Down Expand Up @@ -238,11 +236,23 @@ jobs:
security unlock-keychain -p "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH

security import $DEVELOPER_ID_CERTIFICATE_PATH -P "$DEVELOPER_ID_CERTIFICATE_PASSWORD" -A -t cert -f pkcs12 -k $KEYCHAIN_PATH

# Installer packages need a separate Developer ID Installer identity.
# UNTESTED: productbuild needs prompt-free access to the Installer key for CI;
# verify a signed package after changing the keychain setup.
if [ '${{ matrix.project }}' = 'devolutions-agent' ] && [ -n "$DEVELOPER_ID_INSTALLER_CERTIFICATE" ]; then
DEVELOPER_ID_INSTALLER_CERTIFICATE_PATH=$RUNNER_TEMP/dev_id_installer_cert.p12
echo -n "$DEVELOPER_ID_INSTALLER_CERTIFICATE" | base64 --decode --output $DEVELOPER_ID_INSTALLER_CERTIFICATE_PATH
security import $DEVELOPER_ID_INSTALLER_CERTIFICATE_PATH -P "$DEVELOPER_ID_INSTALLER_CERTIFICATE_PASSWORD" -A -t cert -f pkcs12 -k $KEYCHAIN_PATH
fi

security list-keychain -d user -s $KEYCHAIN_PATH
shell: bash
env:
DEVELOPER_ID_CERTIFICATE: ${{ secrets.APPLE_APP_DEV_ID_APP_CERTIFICATE }}
DEVELOPER_ID_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_APP_DEV_ID_APP_CERTIFICATE_PASSWORD }}
DEVELOPER_ID_INSTALLER_CERTIFICATE: ${{ secrets.APPLE_DEV_ID_INSTALLER_CERTIFICATE }}
DEVELOPER_ID_INSTALLER_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_DEV_ID_INSTALLER_CERTIFICATE_PASSWORD }}

- name: Configure runner (Windows)
if: ${{ matrix.os == 'windows' }}
Expand Down Expand Up @@ -333,15 +343,15 @@ 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', 'DevolutionsPedmShellExt.dll', 'DevolutionsPedmShellExt.msix', 'DevolutionsDesktopAgent.exe', 'devolutions-agent') }
'jetsocat' { @('jetsocat.exe', 'jetsocat') }
})
$ExcludePattern = "*.pdb"
Get-ChildItem -Path ${{ runner.temp }} -Recurse -Include $IncludePattern -Exclude $ExcludePattern | % {
Get-ChildItem -Path $Env:RUNNER_TEMP -Recurse -File -Include $IncludePattern -Exclude $ExcludePattern | % {
if ('${{ matrix.os }}' -Eq 'windows') {
if ($_.Name -Eq 'DevolutionsPedmShellExt.msix') {
$PackagePublisher = '${{ secrets.CODE_SIGNING_APPX_PUBLISHER }}'
$UnpackedMsix = Join-Path ${{ runner.temp }} "unpacked-context-menu-msix"
$UnpackedMsix = Join-Path $Env:RUNNER_TEMP "unpacked-context-menu-msix"
$AppxManifest = Join-Path $UnpackedMsix "AppxManifest.xml"
$PackedMsix = $_.FullName

Expand Down Expand Up @@ -370,16 +380,10 @@ jobs:
'-v')
AzureSignTool @Params $_.FullName
} elseif ('${{ matrix.os }}' -Eq 'macos') {
$SignCmd = $(@(
'codesign',
'--timestamp',
'--options=runtime',
'-s', '"Developer ID Application: Devolutions inc. (N592S9ASDB)"',
'-v',
$_.FullName
)) -Join ' '
Write-Host $SignCmd
Invoke-Expression $SignCmd
codesign --force --timestamp --options=runtime -s 'Developer ID Application: Devolutions inc. (N592S9ASDB)' -v $_.FullName
if ($LastExitCode -Ne 0) {
throw "failed to sign $($_.FullName)"
}
} else {
echo "::debug::nothing to do for ${{ matrix.os }}"
}
Expand Down Expand Up @@ -604,6 +608,71 @@ jobs:
}
shell: pwsh

- name: Repackage and notarize Agent (macOS)
if: ${{ matrix.project == 'devolutions-agent' && matrix.os == 'macos' }}
run: |
$PackageRoot = Join-Path $Env:RUNNER_TEMP devolutions-agent macos
$IsProduction = '${{ needs.preflight.outputs.package-env }}' -Eq 'publish-prod'
$CanSign = -Not [string]::IsNullOrEmpty($Env:DEVELOPER_ID_INSTALLER_CERTIFICATE)
$CanNotarize = $CanSign -And -Not [string]::IsNullOrEmpty($Env:APPLE_BOT_PASSWORD)

if (-Not $CanNotarize) {
$Message = 'the Developer ID Installer certificate or the notarization credentials are missing; the Agent macOS package is not notarized'
if ($IsProduction) { throw $Message }
echo "::warning::$Message"
}

if ($CanSign) {
$Env:DAGENT_PKG_SIGNING_IDENTITY = 'Developer ID Installer: Devolutions inc. (N592S9ASDB)'
}

if ($CanNotarize) {
xcrun notarytool store-credentials 'notarytool-profile' --apple-id "$Env:APPLE_BOT_ID" --team-id "$Env:APPLE_BOT_TEAM_ID" --password "$Env:APPLE_BOT_PASSWORD"
if ($LastExitCode -Ne 0) { throw 'notarytool store-credentials failed' }
}

foreach ($Arch in @('x64', 'arm64')) {
$ArchRoot = Join-Path $PackageRoot $Arch
$Env:DAGENT_EXECUTABLE = Join-Path $ArchRoot 'devolutions-agent'
$Env:TARGET_OUTPUT_PATH = $ArchRoot

Write-Host "=== Repackage Agent ($Arch) ==="
./ci/tlk.ps1 package -Product agent -Platform macos -Architecture $Arch

if ($CanNotarize) {
$PackageFile = Join-Path $ArchRoot 'devolutions-agent.pkg'
xcrun notarytool submit $PackageFile --keychain-profile 'notarytool-profile' --wait
if ($LastExitCode -Ne 0) { throw "notarization failed for $PackageFile" }
xcrun stapler staple $PackageFile
if ($LastExitCode -Ne 0) { throw "stapling failed for $PackageFile" }
}
}
shell: pwsh
env:
DEVELOPER_ID_INSTALLER_CERTIFICATE: ${{ secrets.APPLE_DEV_ID_INSTALLER_CERTIFICATE }}
APPLE_BOT_PASSWORD: ${{ secrets.APPLE_BOT_PASSWORD }}
APPLE_BOT_ID: bot@devolutions.net
APPLE_BOT_TEAM_ID: N592S9ASDB

- name: Verify macOS Agent signatures and notarization
if: ${{ matrix.project == 'devolutions-agent' && matrix.os == 'macos' && needs.preflight.outputs.package-env == 'publish-prod' }}
run: |
$PackageRoot = Join-Path $Env:RUNNER_TEMP devolutions-agent macos
foreach ($Arch in @('x64', 'arm64')) {
$Executable = Join-Path $PackageRoot $Arch 'devolutions-agent'
$Package = Join-Path $PackageRoot $Arch 'devolutions-agent.pkg'

codesign --verify --strict --verbose=2 $Executable
if ($LastExitCode -Ne 0) { throw "failed to verify the signature of $Executable" }

pkgutil --check-signature $Package
if ($LastExitCode -Ne 0) { throw "failed to verify the signature of $Package" }

xcrun stapler validate $Package
if ($LastExitCode -Ne 0) { throw "failed to validate the notarization ticket of $Package" }
}
shell: pwsh

- name: Verification
if: ${{ (matrix.os == 'windows' || matrix.os == 'macos') && env.package-env == 'publish-prod' }}
run: |
Expand All @@ -617,7 +686,7 @@ jobs:
}
}
} elseif ('${{ matrix.os }}' -Eq 'macos') {
Get-ChildItem -Path $RootPath -Recurse -Include 'jetsocat' | % {
Get-ChildItem -Path $RootPath -Recurse -File -Include 'jetsocat' | % {
codesign -dvvv "$($_.FullName)"
if ($LastExitCode -Ne 0) {
echo "::error::failed to verify the signature of $($_.FullName)"
Expand Down
9 changes: 9 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -370,6 +370,11 @@ jobs:
Move-Item "$Dl/devolutions-agent/linux/${Arch}/devolutions-agent.changes" "$Release/devolutions-agent-${Version}-${Arch}.changes"
}

# Agent macOS PKG
foreach ($Arch in @('x64', 'arm64')) {
Move-Item "$Dl/devolutions-agent/macos/${Arch}/devolutions-agent.pkg" "$Release/devolutions-agent-${Version}-${Arch}.pkg"
}

# Jetsocat archives: .tar.xz for Linux, .zip for Windows and macOS
foreach ($P in @(
@{ OS = "windows"; Arch = "x64"; Ext = ".exe"; Archive = "zip" },
Expand Down Expand Up @@ -658,6 +663,10 @@ jobs:
Move-Item -Path "./devolutions-agent/linux/${Arch}/devolutions-agent.rpm" -Destination "$destinationFolder/devolutions-agent-${versionFull}-${Arch}.rpm"
Move-Item -Path "./devolutions-agent/linux/${Arch}/devolutions-agent.changes" -Destination "$destinationFolder/devolutions-agent-${versionFull}-${Arch}.changes"
}

foreach ($Arch in @('x64', 'arm64')) {
Move-Item -Path "./devolutions-agent/macos/${Arch}/devolutions-agent.pkg" -Destination "$destinationFolder/devolutions-agent-${versionFull}-${Arch}.pkg"
}
shell: pwsh

- name: Attest build provenance
Expand Down
85 changes: 85 additions & 0 deletions ci/tlk.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -919,6 +919,89 @@ class TlkRecipe
Pop-Location
}

[void] Package_MacOS() {
if ($this.Product -Ne 'agent') {
throw "macOS packaging for $($this.Product) is not supported"
}

if (Test-Path Env:DAGENT_EXECUTABLE) {
$Executable = $Env:DAGENT_EXECUTABLE
} else {
throw ("Specify DAGENT_EXECUTABLE environment variable")
}

$HostArchitecture = switch ($this.Target.Architecture) {
"x64" { "x86_64" }
"arm64" { "arm64" }
default { throw "unsupported macOS architecture: $($this.Target.Architecture)" }
}

$Identifier = "com.devolutions.agent"
$InputPackagePath = Join-Path $this.SourcePath "package/AgentMacOS"
$OutputPath = Join-Path $this.SourcePath "output/agent-macos-$($this.Target.Architecture)"
$PayloadPath = Join-Path $OutputPath "payload"
$ScriptsPath = Join-Path $OutputPath "scripts"
$BinPath = Join-Path $PayloadPath "usr/local/bin"
$LaunchDaemonsPath = Join-Path $PayloadPath "Library/LaunchDaemons"

Remove-Item -Path $OutputPath -Recurse -Force -ErrorAction SilentlyContinue
@($BinPath, $LaunchDaemonsPath, $ScriptsPath) | % {
New-Item -Path $_ -ItemType 'Directory' -Force | Out-Null
}

Copy-Item $Executable "$BinPath/devolutions-agent"
Copy-Item "$InputPackagePath/devolutions-agent-uninstall" "$BinPath/devolutions-agent-uninstall"
Copy-Item "$InputPackagePath/$Identifier.plist" "$LaunchDaemonsPath/$Identifier.plist"
Copy-Item "$InputPackagePath/scripts/*" $ScriptsPath

& 'chmod' '755' "$BinPath/devolutions-agent" "$BinPath/devolutions-agent-uninstall" "$ScriptsPath/preinstall" "$ScriptsPath/postinstall"
& 'chmod' '644' "$LaunchDaemonsPath/$Identifier.plist"

& 'plutil' '-lint' "$LaunchDaemonsPath/$Identifier.plist" | Out-Host
if ($LASTEXITCODE -Ne 0) {
throw "invalid launchd plist"
}

$ComponentPackageName = "devolutions-agent-component.pkg"

& 'pkgbuild' '--root' $PayloadPath `
'--scripts' $ScriptsPath `
'--identifier' $Identifier `
'--version' $this.Version `
'--install-location' '/' `
'--ownership' 'recommended' `
"$OutputPath/$ComponentPackageName" | Out-Host
if ($LASTEXITCODE -Ne 0) {
throw "pkgbuild failed"
}

$DistributionFile = Join-Path $OutputPath "distribution.xml"
Merge-Tokens -TemplateFile "$InputPackagePath/distribution.xml" -Tokens @{
host_arch = $HostArchitecture
version = $this.Version
component_pkg = $ComponentPackageName
} -OutputFile $DistributionFile

$ProductBuildArgs = @('--distribution', $DistributionFile, '--package-path', $OutputPath)

# The installer is signed here because a signature cannot be added to a built product archive.
if (Test-Path Env:DAGENT_PKG_SIGNING_IDENTITY) {
$ProductBuildArgs += @('--sign', $Env:DAGENT_PKG_SIGNING_IDENTITY, '--timestamp')
}

$PackageFile = Join-Path $OutputPath "devolutions-agent.pkg"
& 'productbuild' @ProductBuildArgs $PackageFile | Out-Host
if ($LASTEXITCODE -Ne 0) {
throw "productbuild failed"
}

if (Test-Path Env:TARGET_OUTPUT_PATH) {
$TargetOutputPath = $Env:TARGET_OUTPUT_PATH
New-Item -Path $TargetOutputPath -ItemType 'Directory' -Force | Out-Null
Copy-Item $PackageFile "$TargetOutputPath/devolutions-agent.pkg" -Force
}
}

[void] Package([string]$PackageOption) {
if ($this.Product -Eq 'jetsocat') {
throw "Packaging for $($this.Product) is not supported"
Expand All @@ -943,6 +1026,8 @@ class TlkRecipe
}
} elseif ($this.Target.IsLinux()) {
$this.Package_Linux()
} elseif ($this.Target.IsMacOS()) {
$this.Package_MacOS()
}
}

Expand Down
10 changes: 8 additions & 2 deletions devolutions-agent/src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,13 @@ use devolutions_agent::AgentServiceEvent;
use devolutions_agent::config::ConfHandle;
use devolutions_agent::enrollment::parse_enrollment_jwt;

use crate::service::{DESCRIPTION, DISPLAY_NAME, SERVICE_NAME};
use crate::service::{DESCRIPTION, DISPLAY_NAME};

/// The launchd label, which must match the LaunchDaemon plist that the macOS package installs.
#[cfg(target_os = "macos")]
const CONTROLLER_NAME: &str = "com.devolutions.agent";
#[cfg(not(target_os = "macos"))]
const CONTROLLER_NAME: &str = crate::service::SERVICE_NAME;

#[derive(Debug, PartialEq, Eq)]
struct UpCommand {
Expand Down Expand Up @@ -97,7 +103,7 @@ fn parse_up_command_args_with_reader<R: BufRead>(args: &[String], mut stdin_read
}

pub(crate) fn run() {
let mut controller = Controller::new(SERVICE_NAME, DISPLAY_NAME, DESCRIPTION);
let mut controller = Controller::new(CONTROLLER_NAME, DISPLAY_NAME, DESCRIPTION);

if let Some(cmd) = env::args().nth(1) {
match cmd.as_str() {
Expand Down
Loading
Loading