Skip to content

Commit 0220fd3

Browse files
CopilotradicaljoperezrCopilotCopilot
authored
[release/13.2] Backport PR #16053: Fix macOS signing, permissions, cert trust, and CI verification (#16215)
* Add macOS JIT entitlements and ad-hoc codesign step for aspire-managed macOS hardened runtime blocks CoreCLR JIT (W^X memory mapping) unless the binary carries com.apple.security.cs.allow-jit and related entitlements. MicroBuild's MacDeveloperHardenWithNotarization signing preserves entitlements from a prior ad-hoc signature, so we codesign with the entitlements plist before Arcade signing. This follows the same pattern used by dotnet/sdk for Roslyn managed binaries (roslyn-entitlements.plist). Fixes #16043 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: joperezr <13854455+joperezr@users.noreply.github.com> * Restore execute permissions on aspire-managed after MicroBuild signing MicroBuild rewrites the binary file during signing, which resets Unix file permissions to the default umask (typically 644). The execute bit must be restored before CreateLayout packs the binary into the CLI archive. Without this, macOS and Linux archives contain a non-executable aspire-managed binary. Fixes #16043 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: joperezr <13854455+joperezr@users.noreply.github.com> * Narrow non-interactive cert trust skip to macOS and Windows only Backport of fbdac8a with conflict resolution: - Added EnsureHttpCertificateExists() to ICertificateToolRunner and NativeCertificateToolRunner - Added ICliHostEnvironment parameter to CertificateService constructor - Updated CliTestHelper default CertificateServiceFactory to pass ICliHostEnvironment - Added IsSuccessfulEnsureResult helper method - Added TestCliHostEnvironment and new non-interactive test cases Co-authored-by: joperezr <13854455+joperezr@users.noreply.github.com> * Add CLI archive verification scripts Add verify-cli-archive.sh (Linux/macOS) and verify-cli-archive.ps1 (Windows) scripts that validate a signed CLI archive by: 1. Extracting the archive to a temp location 2. Running 'aspire --version' to verify the binary executes 3. Running 'aspire new aspire-starter' to test bundle self-extraction and project creation (exercises aspire-managed) 4. Cleaning up temp state (backs up and restores ~/.aspire) These scripts will be wired into the CI pipeline to catch signing and permissions regressions before they reach users. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: joperezr <13854455+joperezr@users.noreply.github.com> * Wire CLI archive verification into CI pipelines Add post-signing verification steps to both build_sign_native.yml (macOS/Linux) and BuildAndTest.yml (Windows) that run the verification scripts after the CLI archives are built and signed. Verification runs for RIDs that can fully execute on the build agent: - macOS (Apple Silicon): osx-arm64 only - Linux (amd64): linux-x64 only - Windows: win-x64 This ensures that signing/permissions regressions (like the ones fixed in this PR) are caught during the official build before release. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: joperezr <13854455+joperezr@users.noreply.github.com> * Address PR feedback: restore testability and fix scripts - Restore mockable isNonInteractiveTrustSupported parameter in CertificateService for testability (was Func<bool> isWindows) - Fix cert tests to use explicit mocks instead of OS-dependent assertions - Move backup dir into VERIFY_TMPDIR to avoid orphaned temp dirs - Fix BuildAndTest.yml comment to match actual script behavior Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: joperezr <13854455+joperezr@users.noreply.github.com> * Add EnsureHttpCertificateExists to shared TestCertificateToolRunner The TestCertificateToolRunner in TestServices/ also needs to implement the EnsureHttpCertificateExists method added to ICertificateToolRunner as part of the backport. Co-authored-by: joperezr <13854455+joperezr@users.noreply.github.com> * Remove non-entitlement changes from backport Keep only macOS entitlements plist and build_sign_native.yml signing changes. Remove CI verification scripts, cert trust changes, and related test updates. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Add CI verification scripts for signed CLI archives Restore BuildAndTest.yml wiring and verify-cli-archive scripts to validate signed archives work correctly after signing. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Ankit Jain <radical@gmail.com> Co-authored-by: joperezr <13854455+joperezr@users.noreply.github.com> Co-authored-by: Copilot <copilot@github.com> Co-authored-by: Jose Perez Rodriguez <joperezr@microsoft.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 491b90f commit 0220fd3

5 files changed

Lines changed: 459 additions & 0 deletions

File tree

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
3+
<plist version="1.0">
4+
<dict>
5+
<!-- Required for CoreCLR JIT compilation under hardened runtime -->
6+
<key>com.apple.security.cs.allow-jit</key>
7+
<true/>
8+
<!-- Required for loading .NET runtime libraries -->
9+
<key>com.apple.security.cs.allow-unsigned-executable-memory</key>
10+
<true/>
11+
<!-- Required for loading non-Apple-signed .NET libraries -->
12+
<key>com.apple.security.cs.disable-library-validation</key>
13+
<true/>
14+
<!-- Required for DYLD_LIBRARY_PATH used by .NET host -->
15+
<key>com.apple.security.cs.allow-dyld-environment-variables</key>
16+
<true/>
17+
</dict>
18+
</plist>

eng/pipelines/templates/BuildAndTest.yml

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,27 @@ steps:
9292
/p:BuildExtension=true
9393
displayName: 🟣Build
9494

95+
# Verify the signed win-x64 CLI archive works (version check + project creation)
96+
- pwsh: |
97+
$ErrorActionPreference = 'Stop'
98+
$archiveDir = "${{ parameters.repoArtifactsPath }}/packages/${{ parameters.buildConfig }}"
99+
$archive = Get-ChildItem -Path $archiveDir -Filter "aspire-cli-win-x64-*.zip" -Recurse -ErrorAction SilentlyContinue | Select-Object -First 1
100+
if (-not $archive) {
101+
Write-Host "##[warning]No win-x64 CLI archive found - skipping verification"
102+
exit 0
103+
}
104+
Write-Host "Found archive: $($archive.FullName)"
105+
& "$(Build.SourcesDirectory)/eng/scripts/verify-cli-archive.ps1" -ArchivePath $archive.FullName
106+
if ($LASTEXITCODE -ne 0) {
107+
Write-Host "##[error]CLI archive verification failed"
108+
exit 1
109+
}
110+
displayName: 🟣Verify CLI archive (win-x64)
111+
condition: succeeded()
112+
env:
113+
ASPIRE_CLI_TELEMETRY_OPTOUT: 'true'
114+
DOTNET_CLI_TELEMETRY_OPTOUT: 'true'
115+
95116
# Log MicroBuild environment for debugging
96117
# MicroBuildOutputFolderOverride is set by the MicroBuildSigningPlugin task in eng/common/templates-official/job/onelocbuild.yml
97118
# which is installed via the Arcade SDK's install-microbuild.yml template that runs before our build steps.

eng/pipelines/templates/build_sign_native.yml

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,18 @@ jobs:
8181
/bl:$(Build.Arcade.LogsPath)PublishManaged.binlog
8282
displayName: 🟣Publish aspire-managed
8383
84+
# On macOS, ad-hoc codesign aspire-managed with JIT entitlements BEFORE Arcade signing.
85+
# MicroBuild (MacDeveloperHardenWithNotarization) preserves entitlements from the prior
86+
# ad-hoc signature when re-signing with the real certificate. Without this step,
87+
# hardened runtime blocks CoreCLR JIT (W^X memory mapping) causing HRESULT: 0x80070008.
88+
# This follows the same pattern used by dotnet/sdk for managed binaries (roslyn-entitlements.plist).
89+
- ${{ if eq(parameters.agentOs, 'macos') }}:
90+
- script: >-
91+
codesign --sign - --force
92+
--entitlements $(Build.SourcesDirectory)/eng/aspire-managed-entitlements.plist
93+
$(Build.SourcesDirectory)/artifacts/bin/Aspire.Managed/Release/net10.0/${{ targetRid }}/publish/aspire-managed
94+
displayName: 🟣Ad-hoc codesign aspire-managed with JIT entitlements
95+
8496
- ${{ if eq(parameters.codeSign, true) }}:
8597
- script: >-
8698
$(Build.SourcesDirectory)/$(scriptName)
@@ -92,6 +104,15 @@ jobs:
92104
env:
93105
SYSTEM_ACCESSTOKEN: $(System.AccessToken)
94106
107+
# On macOS/Linux, restore the execute bit after Arcade signing.
108+
# MicroBuild rewrites the binary file during signing, which resets permissions
109+
# to the default umask (typically 644). The execute bit must be restored before
110+
# CreateLayout packs the binary into the bundle archive.
111+
- ${{ if and(eq(parameters.codeSign, true), ne(parameters.agentOs, 'windows')) }}:
112+
- script: >-
113+
chmod +x $(Build.SourcesDirectory)/artifacts/bin/Aspire.Managed/Release/net10.0/${{ targetRid }}/publish/aspire-managed
114+
displayName: 🟣Restore execute permission on aspire-managed
115+
95116
- script: >-
96117
$(Build.SourcesDirectory)/$(dotnetScript)
97118
msbuild
@@ -119,6 +140,29 @@ jobs:
119140
env:
120141
SYSTEM_ACCESSTOKEN: $(System.AccessToken) # Needed for the signing task
121142
143+
# Verify the signed CLI archive can execute and create a project.
144+
# Run for RIDs that can fully execute on the build agent:
145+
# macOS (Apple Silicon): osx-arm64 only (osx-x64 via Rosetta can run the CLI
146+
# but aspire-managed template resolution fails)
147+
# Linux (amd64): linux-x64 only (linux-arm64/linux-musl-x64 are cross-compiled)
148+
- ${{ if or(and(eq(parameters.agentOs, 'macos'), eq(targetRid, 'osx-arm64')), and(eq(parameters.agentOs, 'linux'), eq(targetRid, 'linux-x64'))) }}:
149+
- script: |
150+
set -euo pipefail
151+
echo "Finding CLI archive for ${{ targetRid }}..."
152+
ARCHIVE=$(find "$(Build.SourcesDirectory)/artifacts/packages/$(_BuildConfig)" -name "aspire-cli-${{ targetRid }}-*.tar.gz" -type f | head -1)
153+
if [ -z "$ARCHIVE" ]; then
154+
echo "##[error]No CLI archive found for ${{ targetRid }}"
155+
exit 1
156+
fi
157+
echo "Found archive: $ARCHIVE"
158+
chmod +x "$(Build.SourcesDirectory)/eng/scripts/verify-cli-archive.sh"
159+
"$(Build.SourcesDirectory)/eng/scripts/verify-cli-archive.sh" "$ARCHIVE"
160+
displayName: 🟣Verify CLI archive (${{ targetRid }})
161+
condition: succeeded()
162+
env:
163+
ASPIRE_CLI_TELEMETRY_OPTOUT: 'true'
164+
DOTNET_CLI_TELEMETRY_OPTOUT: 'true'
165+
122166
- task: 1ES.PublishBuildArtifacts@1
123167
displayName: 🟣Publish Artifacts
124168
condition: always()

eng/scripts/verify-cli-archive.ps1

Lines changed: 171 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,171 @@
1+
<#
2+
.SYNOPSIS
3+
Verify that a signed Aspire CLI archive produces a working binary.
4+
5+
.DESCRIPTION
6+
This script:
7+
1. Cleans ~/.aspire to ensure no stale state
8+
2. Extracts the CLI archive to a temp location
9+
3. Runs 'aspire --version' to validate the binary executes
10+
4. Runs 'aspire new aspire-starter' to test bundle self-extraction + project creation
11+
5. Cleans up temp directories
12+
13+
.PARAMETER ArchivePath
14+
Path to the CLI archive (.zip or .tar.gz)
15+
16+
.EXAMPLE
17+
.\verify-cli-archive.ps1 -ArchivePath "artifacts\packages\Release\Shipping\aspire-cli-win-x64-10.0.0.zip"
18+
#>
19+
20+
param(
21+
[Parameter(Mandatory = $true, Position = 0)]
22+
[string]$ArchivePath
23+
)
24+
25+
$ErrorActionPreference = 'Stop'
26+
27+
function Write-Step { param([string]$msg) Write-Host "$msg" -ForegroundColor Cyan }
28+
function Write-Ok { param([string]$msg) Write-Host "$msg" -ForegroundColor Green }
29+
function Write-Err { param([string]$msg) Write-Host "$msg" -ForegroundColor Red }
30+
31+
$verifyTmpDir = $null
32+
$aspireBackup = $null
33+
34+
function Invoke-Cleanup {
35+
if ($verifyTmpDir -and (Test-Path $verifyTmpDir)) {
36+
Write-Step "Cleaning up temp directory: $verifyTmpDir"
37+
Remove-Item -Recurse -Force $verifyTmpDir -ErrorAction SilentlyContinue
38+
}
39+
# Restore ~/.aspire if we backed it up
40+
$aspireDir = Join-Path $env:USERPROFILE ".aspire"
41+
if ($aspireBackup -and (Test-Path $aspireBackup)) {
42+
if (Test-Path $aspireDir) {
43+
Remove-Item -Recurse -Force $aspireDir -ErrorAction SilentlyContinue
44+
}
45+
Move-Item $aspireBackup $aspireDir
46+
Write-Step "Restored original ~/.aspire"
47+
}
48+
}
49+
50+
try {
51+
# Validate archive exists
52+
if (-not (Test-Path $ArchivePath)) {
53+
Write-Err "Archive not found: $ArchivePath"
54+
exit 1
55+
}
56+
57+
$ArchivePath = (Resolve-Path $ArchivePath).Path
58+
59+
# Suppress interactive prompts and telemetry
60+
$env:ASPIRE_CLI_TELEMETRY_OPTOUT = "true"
61+
$env:DOTNET_CLI_TELEMETRY_OPTOUT = "true"
62+
$env:DOTNET_SKIP_FIRST_TIME_EXPERIENCE = "true"
63+
$env:DOTNET_GENERATE_ASPNET_CERTIFICATE = "false"
64+
65+
Write-Host ""
66+
Write-Host "=========================================="
67+
Write-Host " Aspire CLI Archive Verification"
68+
Write-Host "=========================================="
69+
Write-Host " Archive: $ArchivePath"
70+
Write-Host "=========================================="
71+
Write-Host ""
72+
73+
# Step 1: Back up and clean ~/.aspire
74+
Write-Step "Cleaning ~/.aspire state..."
75+
$aspireDir = Join-Path $env:USERPROFILE ".aspire"
76+
if (Test-Path $aspireDir) {
77+
$aspireBackup = Join-Path ([System.IO.Path]::GetTempPath()) "aspire-backup-$([System.IO.Path]::GetRandomFileName())"
78+
Move-Item $aspireDir $aspireBackup
79+
Write-Step "Backed up existing ~/.aspire to $aspireBackup"
80+
}
81+
Write-Ok "Clean ~/.aspire state"
82+
83+
# Step 2: Extract the archive
84+
$verifyTmpDir = Join-Path ([System.IO.Path]::GetTempPath()) "aspire-verify-$([System.IO.Path]::GetRandomFileName())"
85+
$extractDir = Join-Path $verifyTmpDir "cli"
86+
New-Item -ItemType Directory -Path $extractDir -Force | Out-Null
87+
88+
Write-Step "Extracting archive to $extractDir..."
89+
if ($ArchivePath.EndsWith(".zip")) {
90+
Expand-Archive -Path $ArchivePath -DestinationPath $extractDir
91+
}
92+
elseif ($ArchivePath.EndsWith(".tar.gz")) {
93+
tar -xzf $ArchivePath -C $extractDir
94+
if ($LASTEXITCODE -ne 0) {
95+
Write-Err "Failed to extract tar.gz archive"
96+
exit 1
97+
}
98+
}
99+
else {
100+
Write-Err "Unsupported archive format: $ArchivePath (expected .zip or .tar.gz)"
101+
exit 1
102+
}
103+
104+
# Find the aspire binary
105+
$aspireBin = Join-Path $extractDir "aspire.exe"
106+
if (-not (Test-Path $aspireBin)) {
107+
$aspireBin = Join-Path $extractDir "aspire"
108+
if (-not (Test-Path $aspireBin)) {
109+
Write-Err "Could not find 'aspire' binary in extracted archive."
110+
Get-ChildItem $extractDir | Format-Table
111+
exit 1
112+
}
113+
}
114+
Write-Ok "Extracted CLI binary: $aspireBin"
115+
116+
# Install to ~/.aspire/bin so self-extraction works correctly
117+
Write-Step "Installing CLI to ~/.aspire/bin..."
118+
$aspireDir = Join-Path $env:USERPROFILE ".aspire"
119+
$aspireBinDir = Join-Path $aspireDir "bin"
120+
New-Item -ItemType Directory -Path $aspireBinDir -Force | Out-Null
121+
Copy-Item $aspireBin (Join-Path $aspireBinDir (Split-Path $aspireBin -Leaf))
122+
$aspireBin = Join-Path $aspireBinDir (Split-Path $aspireBin -Leaf)
123+
$env:PATH = "$aspireBinDir;$env:PATH"
124+
Write-Ok "CLI installed to ~/.aspire/bin"
125+
126+
# Step 3: Verify aspire --version
127+
Write-Step "Running 'aspire --version'..."
128+
$versionOutput = & $aspireBin --version 2>&1
129+
if ($LASTEXITCODE -ne 0) {
130+
Write-Err "'aspire --version' failed with exit code $LASTEXITCODE"
131+
Write-Host "Output: $versionOutput"
132+
exit 1
133+
}
134+
Write-Host " Version: $versionOutput"
135+
Write-Ok "'aspire --version' succeeded"
136+
137+
# Step 4: Create a new project with aspire new
138+
# This exercises bundle self-extraction and aspire-managed (template search + download + scaffolding)
139+
$projectDir = Join-Path $verifyTmpDir "VerifyApp"
140+
New-Item -ItemType Directory -Path $projectDir -Force | Out-Null
141+
142+
Write-Step "Running 'aspire new aspire-starter --name VerifyApp --output $projectDir --non-interactive --nologo'..."
143+
& $aspireBin new aspire-starter --name VerifyApp --output $projectDir --non-interactive --nologo 2>&1 | Write-Host
144+
if ($LASTEXITCODE -ne 0) {
145+
Write-Err "'aspire new' failed with exit code $LASTEXITCODE"
146+
exit 1
147+
}
148+
149+
# Verify the project was created
150+
$appHostDir = Join-Path $projectDir "VerifyApp.AppHost"
151+
if (-not (Test-Path $appHostDir)) {
152+
Write-Err "Expected project directory 'VerifyApp.AppHost' not found after 'aspire new'"
153+
Get-ChildItem $projectDir | Format-Table
154+
exit 1
155+
}
156+
Write-Ok "'aspire new' created project successfully"
157+
158+
Write-Host ""
159+
Write-Host "=========================================="
160+
Write-Host " All verification checks passed!" -ForegroundColor Green
161+
Write-Host "=========================================="
162+
Write-Host ""
163+
}
164+
catch {
165+
Write-Err "Verification failed: $_"
166+
Write-Host $_.ScriptStackTrace
167+
exit 1
168+
}
169+
finally {
170+
Invoke-Cleanup
171+
}

0 commit comments

Comments
 (0)