diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..db76a10 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,177 @@ +# Release +# +# Produces the signed, single-exe installer (dist\MicroClawSetup.exe) and +# publishes it as a GitHub Release asset. +# +# build.ps1 Step 7 wraps the PyInstaller onedir in an NSIS self-extractor +# (installer\microclaw-setup.nsi) and then calls scripts\windows\sign-artifact.ps1. +# That signer is a NO-OP unless the TRUSTED_SIGNING_* / AZURE_* secrets below +# are present, so the *signed* exe is only ever produced by this workflow — +# never on PRs (which have no secrets and may run on forks). +# +# Signing the single downloaded exe is what clears Windows SmartScreen: +# files a trusted local process later extracts carry no Mark-of-the-Web and +# are not re-evaluated. +# +# Triggers: +# - push of a version tag (v*) -> publishes a release for that tag +# - manual workflow_dispatch -> builds + signs, uploads as a run artifact +name: "Release" + +on: + push: + tags: ["v*"] + workflow_dispatch: + inputs: + sign: + description: "Code-sign MicroClawSetup.exe (uncheck to get an UNSIGNED exe while signing is still under discussion)" + type: boolean + default: false + publish_release: + description: "Create a GitHub Release (only meaningful on a tag)" + type: boolean + default: false + +concurrency: + group: release-${{ github.ref }} + cancel-in-progress: false + +permissions: + contents: write # needed to create the release and upload assets + +jobs: + build-signed: + name: Build + sign single-exe installer + runs-on: windows-latest + timeout-minutes: 60 + defaults: + run: + shell: pwsh + + env: + # Azure Trusted Signing — consumed by scripts\windows\sign-artifact.ps1. + # If any of the three TRUSTED_SIGNING_* values is empty the signer skips + # (no-op, exit 0) and the exe is produced UNSIGNED. + # + # Signing is enabled for tag builds, and for manual (workflow_dispatch) + # runs only when the "sign" input is checked. When disabled we pass empty + # strings so the signer no-ops and you get an unsigned MicroClawSetup.exe + # (useful while the signing approach is still under discussion). + TRUSTED_SIGNING_ENDPOINT: ${{ (github.event_name != 'workflow_dispatch' || inputs.sign) && secrets.TRUSTED_SIGNING_ENDPOINT || '' }} + TRUSTED_SIGNING_ACCOUNT: ${{ (github.event_name != 'workflow_dispatch' || inputs.sign) && secrets.TRUSTED_SIGNING_ACCOUNT || '' }} + TRUSTED_SIGNING_PROFILE: ${{ (github.event_name != 'workflow_dispatch' || inputs.sign) && secrets.TRUSTED_SIGNING_PROFILE || '' }} + # DefaultAzureCredential for the Trusted Signing client. + AZURE_TENANT_ID: ${{ (github.event_name != 'workflow_dispatch' || inputs.sign) && secrets.AZURE_TENANT_ID || '' }} + AZURE_CLIENT_ID: ${{ (github.event_name != 'workflow_dispatch' || inputs.sign) && secrets.AZURE_CLIENT_ID || '' }} + AZURE_CLIENT_SECRET: ${{ (github.event_name != 'workflow_dispatch' || inputs.sign) && secrets.AZURE_CLIENT_SECRET || '' }} + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + # GitHub-hosted windows-latest ships older/other toolchain versions than + # build.ps1 expects, so pin them explicitly with the setup-* actions + # (reliable on hosted runners, unlike on the self-hosted NetworkService + # account). build.ps1 then installs its own npm/pip deps and locates NSIS + # from electron-builder's cache. + - name: Setup Node.js 22 + uses: actions/setup-node@v4 + with: + node-version: "22" + + - name: Setup .NET 9 SDK + uses: actions/setup-dotnet@v4 + with: + dotnet-version: "9.0.x" + + - name: Setup Python 3.12 + uses: actions/setup-python@v5 + with: + python-version: "3.12" + + - name: Verify toolchain + run: | + $ErrorActionPreference = 'Stop' + $node = (& node --version).TrimStart('v') + if ([version]$node -lt [version]'22.0') { throw "Node $node < 22" } + Write-Host "node $node" + $dotnet = & dotnet --version + if ([version]($dotnet -split '-')[0] -lt [version]'9.0') { throw "dotnet $dotnet < 9.0" } + Write-Host "dotnet $dotnet" + Write-Host "python $(& python --version)" + + - name: Install Python build dependencies + run: | + python -m pip install --upgrade pip + python -m pip install -r requirements.txt + + # build.ps1 Step 7 needs makensis. electron-builder populates its NSIS + # cache during Step 3, so it is normally present by Step 7. Pre-install + # a system copy as a fallback so a cold runner never fails the step. + - name: Ensure NSIS is available + run: | + $ErrorActionPreference = 'SilentlyContinue' + $have = Get-Command makensis -ErrorAction SilentlyContinue + $sys = Test-Path "${env:ProgramFiles(x86)}\NSIS\makensis.exe" + if (-not $have -and -not $sys) { + Write-Host "Installing NSIS via choco..." + choco install nsis -y --no-progress + } else { + Write-Host "NSIS already present." + } + + - name: Run build.ps1 (builds + signs MicroClawSetup.exe) + run: .\build.ps1 + + - name: Verify build artifacts + run: | + $ErrorActionPreference = 'Stop' + $expected = @( + 'dist\microclaw-portable.zip', + 'dist\MicroClawInstaller.zip', + 'dist\MicroClawSetup.exe' + ) + $missing = $expected | Where-Object { -not (Test-Path $_) } + if ($missing) { + Write-Host "::error::Missing expected artifacts: $($missing -join ', ')" + exit 1 + } + Get-ChildItem dist | Format-Table Name, Length, LastWriteTime + + # Confirm the single-exe installer is Authenticode-signed. When signing + # secrets are configured this must be Valid; otherwise we surface a clear + # warning (a tag build without secrets ships an unsigned exe that will + # still trip SmartScreen). + - name: Verify signature + run: | + $sig = Get-AuthenticodeSignature 'dist\MicroClawSetup.exe' + Write-Host "MicroClawSetup.exe signature status: $($sig.Status)" + if ($sig.SignerCertificate) { + Write-Host "Signer: $($sig.SignerCertificate.Subject)" + } + $configured = $env:TRUSTED_SIGNING_ENDPOINT -and $env:TRUSTED_SIGNING_ACCOUNT -and $env:TRUSTED_SIGNING_PROFILE + if ($configured) { + if ($sig.Status -ne 'Valid') { + Write-Host "::error::Trusted Signing was configured but the exe is not validly signed ($($sig.Status))." + exit 1 + } + Write-Host "Signed installer verified." + } else { + Write-Host "::warning::MicroClawSetup.exe is UNSIGNED (signing toggle off or secrets not configured) and will trip SmartScreen. This is expected while the signing approach is under discussion." + } + + - name: Upload build artifact + uses: actions/upload-artifact@v4 + with: + name: MicroClawSetup + path: dist\MicroClawSetup.exe + if-no-files-found: error + + # Publish a GitHub Release with the signed exe when built from a tag. + - name: Publish GitHub Release + if: startsWith(github.ref, 'refs/tags/v') + uses: softprops/action-gh-release@v2 + with: + files: dist\MicroClawSetup.exe + fail_on_unmatched_files: true + generate_release_notes: true diff --git a/build.ps1 b/build.ps1 index 7464027..d155538 100644 --- a/build.ps1 +++ b/build.ps1 @@ -47,7 +47,7 @@ if (-not $nodeFound) { } # -- Step 1: Build AppContainerLauncher.exe (.NET 9) -- -Write-Host "`n=== Step 1/6: Build AppContainerLauncher ===" -ForegroundColor Cyan +Write-Host "`n=== Step 1/7: Build AppContainerLauncher ===" -ForegroundColor Cyan $acProject = "$root\appcontainer" if (-not (Test-Path "$acProject\AppContainerLauncher.csproj")) { Write-Host " ERROR: appcontainer project not found at $acProject" -ForegroundColor Red @@ -88,7 +88,7 @@ if (Test-Path $preloadSrc) { } # -- Step 2: Clean dist/ to prevent stale TypeScript output -- -Write-Host "`n=== Step 2/6: Clean stale build artifacts ===" -ForegroundColor Cyan +Write-Host "`n=== Step 2/7: Clean stale build artifacts ===" -ForegroundColor Cyan $distDir = "$root\desktop\dist" if (Test-Path $distDir) { Remove-Item "$distDir\*.js" -Force -ErrorAction SilentlyContinue @@ -104,7 +104,7 @@ if (-not (Test-Path $outDist)) { } # -- Step 3: Build & pack desktop -- -Write-Host "`n=== Step 3/6: Build & pack desktop ===" -ForegroundColor Cyan +Write-Host "`n=== Step 3/7: Build & pack desktop ===" -ForegroundColor Cyan Push-Location "$root\desktop" try { # First-run bootstrap: install npm deps (including renderer via postinstall) @@ -161,7 +161,7 @@ if (Test-Path $desktopAsar) { } # Step 4: Create portable zip -Write-Host "`n=== Step 4/6: Create portable zip ===" -ForegroundColor Cyan +Write-Host "`n=== Step 4/7: Create portable zip ===" -ForegroundColor Cyan $zipPath = "$root\dist\microclaw-portable.zip" if (Test-Path $zipPath) { Remove-Item $zipPath -Force } Compress-Archive -Path "$root\desktop\release\win-unpacked\*" -DestinationPath $zipPath @@ -169,7 +169,7 @@ $zipSizeMB = [math]::Round((Get-Item $zipPath).Length / 1MB, 1) Write-Host " -> $zipPath ${zipSizeMB} MB" # Step 5: Build installer (onedir mode to avoid WDAC blocking DLLs from temp) -Write-Host "`n=== Step 5/6: Build installer ===" -ForegroundColor Cyan +Write-Host "`n=== Step 5/7: Build installer ===" -ForegroundColor Cyan Push-Location $root $installerBuilt = $false @@ -260,7 +260,7 @@ if (-not $installerBuilt) { } # Step 6: Pack onedir output into a single distributable zip -Write-Host "`n=== Step 6/6: Pack installer directory ===" -ForegroundColor Cyan +Write-Host "`n=== Step 6/7: Pack installer directory ===" -ForegroundColor Cyan $installerDir = "$root\dist\MicroClawInstaller" $installerZip = "$root\dist\MicroClawInstaller.zip" if (-not (Test-Path $installerDir)) { @@ -273,7 +273,79 @@ Compress-Archive -Path "$installerDir\*" -DestinationPath $installerZip $instZipSizeMB = [math]::Round((Get-Item $installerZip).Length / 1MB, 1) Write-Host " -> $installerZip ${instZipSizeMB} MB" -ForegroundColor Green +# Step 7: Build the single-exe setup (NSIS self-extractor) and code-sign it. +# This is the ONE file end users download. It extracts the onedir installer to a +# real directory under %LOCALAPPDATA% (not %TEMP%, preserving WDAC safety) and +# auto-launches MicroClawInstaller.exe. Only this stub needs signing to clear +# SmartScreen (it is the only file that carries Mark-of-the-Web on download). +Write-Host "`n=== Step 7/7: Build single-exe setup + sign ===" -ForegroundColor Cyan +$setupExe = "$root\dist\MicroClawSetup.exe" +$nsiScript = "$root\installer\microclaw-setup.nsi" +$setupIcon = "$root\deployer\assets\microclaw.ico" + +# Resolve makensis: PATH first, then common install locations, then the copy +# that ships inside electron-builder's cache (already present after Step 3). +$makensis = $null +$mkCmd = Get-Command makensis -ErrorAction SilentlyContinue +if ($mkCmd) { $makensis = $mkCmd.Source } +if (-not $makensis) { + $mkCandidates = @( + "${env:ProgramFiles(x86)}\NSIS\makensis.exe", + "$env:ProgramFiles\NSIS\makensis.exe" + ) + $mkCandidates += Get-ChildItem "$env:LOCALAPPDATA\electron-builder\Cache\nsis" -Recurse -Filter makensis.exe -ErrorAction SilentlyContinue | + Select-Object -ExpandProperty FullName + $makensis = $mkCandidates | Where-Object { $_ -and (Test-Path $_) } | Select-Object -First 1 +} + +if (-not $makensis) { + Write-Host " ERROR: makensis (NSIS) not found. Install NSIS 3.x (e.g. 'choco install nsis')." -ForegroundColor Red + Write-Host " The onedir installer + zip were still produced." -ForegroundColor Yellow + exit 1 +} +Write-Host " Using makensis: $makensis" + +# Derive a 4-part version (NSIS VIProductVersion requires X.X.X.X). +$pkgVersion = '0.0.0' +try { + $pkgVersion = (Get-Content "$root\desktop\package.json" -Raw | ConvertFrom-Json).version +} catch { } +$verParts = @($pkgVersion -split '\.') + @('0','0','0','0') +$version4 = ($verParts[0..3]) -join '.' + +if (Test-Path $setupExe) { Remove-Item $setupExe -Force } + +$prev = $ErrorActionPreference +$ErrorActionPreference = "Continue" +& $makensis ` + "/DPAYLOAD_DIR=$installerDir" ` + "/DOUT_FILE=$setupExe" ` + "/DICON=$setupIcon" ` + "/DVERSION=$version4" ` + $nsiScript 2>&1 | ForEach-Object { Write-Host " $_" } +$nsisExit = $LASTEXITCODE +$ErrorActionPreference = $prev +if ($nsisExit -ne 0 -or -not (Test-Path $setupExe)) { + Write-Host " ERROR: makensis failed (exit $nsisExit) — MicroClawSetup.exe not produced." -ForegroundColor Red + exit 1 +} +$setupSizeMB = [math]::Round((Get-Item $setupExe).Length / 1MB, 1) +Write-Host " -> $setupExe ${setupSizeMB} MB" -ForegroundColor Green + +# Code-sign the setup exe. This is a NO-OP unless Trusted Signing is configured +# (TRUSTED_SIGNING_ENDPOINT/ACCOUNT/PROFILE), so local + PR builds are unchanged. +# Run in a child process using the SAME PowerShell host (the signer calls +# `exit`, which would otherwise terminate this build script). +$psHost = (Get-Process -Id $PID).Path +if (-not $psHost) { $psHost = 'powershell' } +& $psHost -NoProfile -File "$root\scripts\windows\sign-artifact.ps1" -Path $setupExe +if ($LASTEXITCODE -ne 0) { + Write-Host " ERROR: signing step failed (exit $LASTEXITCODE)." -ForegroundColor Red + exit 1 +} + Write-Host "`n=== Done ===" -ForegroundColor Green -Write-Host " Installer dir: $root\dist\MicroClawInstaller\" -Write-Host " Installer zip: $installerZip" -Write-Host " Portable: $zipPath" +Write-Host " Setup (one-exe): $setupExe" +Write-Host " Installer dir: $root\dist\MicroClawInstaller\" +Write-Host " Installer zip: $installerZip" +Write-Host " Portable: $zipPath" diff --git a/installer/microclaw-setup.nsi b/installer/microclaw-setup.nsi new file mode 100644 index 0000000..e24a32b --- /dev/null +++ b/installer/microclaw-setup.nsi @@ -0,0 +1,80 @@ +; MicroClaw single-exe setup stub (NSIS) +; --------------------------------------------------------------------------- +; Wraps the PyInstaller *onedir* installer (dist\MicroClawInstaller\) into ONE +; downloadable, code-signable .exe. On launch it: +; 1. clears any stale staging from a previous run, +; 2. extracts the onedir payload to a REAL directory under %LOCALAPPDATA% +; (deliberately NOT %TEMP% — this preserves the WDAC-safe posture that +; the team chose onedir for; unsigned bundled DLLs never load from %TEMP%), +; 3. auto-launches MicroClawInstaller.exe (the existing web installer UI), +; 4. exits, leaving the installer running — no manual step required. +; +; Only THIS stub needs an Authenticode signature to clear SmartScreen: it is +; the single file the user downloads (and therefore the only one that carries +; Mark-of-the-Web). Files it extracts are created by a trusted local process, +; so they carry no MOTW and are not re-evaluated by SmartScreen. +; +; Build-time defines (passed by build.ps1): +; PAYLOAD_DIR absolute path to dist\MicroClawInstaller (the onedir) +; OUT_FILE absolute path of the .exe to produce (dist\MicroClawSetup.exe) +; ICON absolute path to the app .ico +; VERSION 4-part version string, e.g. 1.0.0.0 +; --------------------------------------------------------------------------- + +Unicode true + +!ifndef PAYLOAD_DIR + !error "PAYLOAD_DIR is required (path to the onedir installer)." +!endif +!ifndef OUT_FILE + !define OUT_FILE "MicroClawSetup.exe" +!endif +!ifndef VERSION + !define VERSION "0.0.0.0" +!endif + +Name "MicroClaw Setup" +OutFile "${OUT_FILE}" +!ifdef ICON + Icon "${ICON}" +!endif + +; Extraction only — no admin needed. The inner installer self-elevates the +; individual steps (e.g. the Node.js MSI) when required. +RequestExecutionLevel user +SetCompressor /SOLID lzma + +InstallDir "$LOCALAPPDATA\MicroClaw\Setup" +AutoCloseWindow true +ShowInstDetails show +BrandingText "MicroClaw" + +; Version resource — a proper version block improves the signed file's +; presentation in SmartScreen / UAC / file properties. +VIProductVersion "${VERSION}" +VIAddVersionKey "ProductName" "MicroClaw Setup" +VIAddVersionKey "FileDescription" "MicroClaw Installer" +VIAddVersionKey "FileVersion" "${VERSION}" +VIAddVersionKey "ProductVersion" "${VERSION}" +VIAddVersionKey "CompanyName" "MicroClaw" +VIAddVersionKey "LegalCopyright" "Copyright (C) 2026 MicroClaw" + +Page instfiles + +Section "Install" + ; Bound disk usage to a single copy: wipe any leftover staging first. + RMDir /r "$INSTDIR" + SetOutPath "$INSTDIR" + SetOverwrite on + + DetailPrint "Extracting MicroClaw installer..." + File /r "${PAYLOAD_DIR}\*" + + IfFileExists "$INSTDIR\MicroClawInstaller.exe" +3 0 + MessageBox MB_ICONSTOP "Setup payload is incomplete: MicroClawInstaller.exe was not found." + Abort "Missing MicroClawInstaller.exe" + + ; Fire-and-forget: launch the real installer, then let this stub close. + DetailPrint "Starting MicroClaw installer..." + Exec '"$INSTDIR\MicroClawInstaller.exe"' +SectionEnd diff --git a/scripts/windows/sign-artifact.ps1 b/scripts/windows/sign-artifact.ps1 new file mode 100644 index 0000000..7d5928c --- /dev/null +++ b/scripts/windows/sign-artifact.ps1 @@ -0,0 +1,151 @@ +<# +.SYNOPSIS + Authenticode-sign one or more files with Azure Trusted Signing (via signtool). + +.DESCRIPTION + This is the code-signing step for MicroClaw's release pipeline. It signs the + single downloadable installer (dist\MicroClawSetup.exe) so Microsoft Defender + SmartScreen no longer blocks it as an "unrecognized app". + + IMPORTANT — this script is OPT-IN and a NO-OP unless Trusted Signing is + configured via environment variables. That keeps local developer builds and + PR/CI builds (which have no signing secrets, and may run on forks) completely + unchanged: they simply produce an unsigned artifact, exactly as before. + + Signing is enabled only when ALL of these are present: + TRUSTED_SIGNING_ENDPOINT e.g. https://wus2.codesigning.azure.net + TRUSTED_SIGNING_ACCOUNT your Trusted Signing account name + TRUSTED_SIGNING_PROFILE the certificate profile name + + Azure authentication is handled by the Trusted Signing dlib via + Azure.Identity's DefaultAzureCredential. In CI, set a service principal: + AZURE_TENANT_ID / AZURE_CLIENT_ID / AZURE_CLIENT_SECRET + Locally you can instead use `az login` or a managed identity. + + Requirements (auto-resolved where possible): + * signtool.exe from a Windows 10/11 SDK build >= 10.0.22621 (dlib support). + * The Azure.CodeSigning.Dlib.dll from the NuGet package + `Microsoft.Trusted.Signing.Client` (downloaded + cached on first use). + +.PARAMETER Path + One or more files to sign. + +.EXAMPLE + pwsh scripts\windows\sign-artifact.ps1 -Path dist\MicroClawSetup.exe +#> +[CmdletBinding()] +param( + [Parameter(Mandatory = $true, Position = 0)] + [string[]]$Path, + + [string]$Endpoint = $env:TRUSTED_SIGNING_ENDPOINT, + [string]$Account = $env:TRUSTED_SIGNING_ACCOUNT, + [string]$CertProfile = $env:TRUSTED_SIGNING_PROFILE, + [string]$TimestampUrl = $(if ($env:TRUSTED_SIGNING_TIMESTAMP_URL) { $env:TRUSTED_SIGNING_TIMESTAMP_URL } else { 'http://timestamp.acs.microsoft.com' }), + [string]$ClientVersion = $(if ($env:TRUSTED_SIGNING_CLIENT_VERSION) { $env:TRUSTED_SIGNING_CLIENT_VERSION } else { '1.0.86' }) +) + +$ErrorActionPreference = 'Stop' + +function Write-Info($m) { Write-Host " [sign] $m" } + +# --- Gate: skip cleanly when Trusted Signing is not configured --------------- +if (-not ($Endpoint -and $Account -and $CertProfile)) { + Write-Info 'Trusted Signing not configured (TRUSTED_SIGNING_ENDPOINT/ACCOUNT/PROFILE unset) - skipping code signing.' + Write-Info 'Artifacts will be produced UNSIGNED. This is expected for local and PR builds.' + exit 0 +} + +# --- Resolve a signtool.exe new enough for /dlib (Trusted Signing) ----------- +function Resolve-SignTool { + if ($env:SIGNTOOL_PATH -and (Test-Path $env:SIGNTOOL_PATH)) { return $env:SIGNTOOL_PATH } + + $kitBins = @( + "${env:ProgramFiles(x86)}\Windows Kits\10\bin", + "$env:ProgramFiles\Windows Kits\10\bin" + ) | Where-Object { Test-Path $_ } + + $candidates = foreach ($root in $kitBins) { + Get-ChildItem $root -Directory -ErrorAction SilentlyContinue | + Where-Object { $_.Name -match '^10\.' } | + ForEach-Object { + $exe = Join-Path $_.FullName 'x64\signtool.exe' + if (Test-Path $exe) { [pscustomobject]@{ Version = [version]$_.Name; Path = $exe } } + } + } + # Trusted Signing dlib requires SDK build >= 10.0.22621. + $best = $candidates | Where-Object { $_.Version -ge [version]'10.0.22621.0' } | + Sort-Object Version -Descending | Select-Object -First 1 + if (-not $best) { + throw "No signtool.exe (Windows SDK >= 10.0.22621) found. Install the Windows 11 SDK, or set SIGNTOOL_PATH." + } + return $best.Path +} + +# --- Ensure the Azure Trusted Signing dlib is available (cached) -------------- +function Resolve-Dlib { + param([string]$Version) + + $cacheRoot = Join-Path $env:LOCALAPPDATA "MicroClaw\trusted-signing\$Version" + $existing = Get-ChildItem $cacheRoot -Recurse -Filter 'Azure.CodeSigning.Dlib.dll' -ErrorAction SilentlyContinue | + Select-Object -First 1 + if ($existing) { return $existing.FullName } + + New-Item -ItemType Directory -Force -Path $cacheRoot | Out-Null + $nupkg = Join-Path $cacheRoot "package.zip" + $url = "https://www.nuget.org/api/v2/package/Microsoft.Trusted.Signing.Client/$Version" + Write-Info "Downloading Microsoft.Trusted.Signing.Client $Version ..." + [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 + Invoke-WebRequest -Uri $url -OutFile $nupkg -UseBasicParsing + + $extractDir = Join-Path $cacheRoot 'pkg' + if (Test-Path $extractDir) { Remove-Item $extractDir -Recurse -Force } + Expand-Archive -Path $nupkg -DestinationPath $extractDir -Force + + $dll = Get-ChildItem $extractDir -Recurse -Filter 'Azure.CodeSigning.Dlib.dll' -ErrorAction SilentlyContinue | + Where-Object { $_.FullName -match '\\x64\\' } | Select-Object -First 1 + if (-not $dll) { + $dll = Get-ChildItem $extractDir -Recurse -Filter 'Azure.CodeSigning.Dlib.dll' -ErrorAction SilentlyContinue | + Select-Object -First 1 + } + if (-not $dll) { + throw "Azure.CodeSigning.Dlib.dll not found inside Microsoft.Trusted.Signing.Client $Version." + } + return $dll.FullName +} + +$signtool = Resolve-SignTool +$dlib = Resolve-Dlib -Version $ClientVersion +Write-Info "signtool: $signtool" +Write-Info "dlib: $dlib" + +# --- Trusted Signing metadata file (account + certificate profile) ----------- +$metadata = @{ + Endpoint = $Endpoint + CodeSigningAccountName = $Account + CertificateProfileName = $CertProfile +} | ConvertTo-Json +$metadataFile = Join-Path ([IO.Path]::GetTempPath()) "trusted-signing-$([guid]::NewGuid().ToString('N')).json" +Set-Content -Path $metadataFile -Value $metadata -Encoding UTF8 + +try { + foreach ($file in $Path) { + if (-not (Test-Path $file)) { throw "File to sign not found: $file" } + $full = (Resolve-Path $file).Path + Write-Info "Signing $full" + & $signtool sign /v /debug /fd SHA256 ` + /tr $TimestampUrl /td SHA256 ` + /dlib $dlib /dmdf $metadataFile ` + $full + if ($LASTEXITCODE -ne 0) { throw "signtool sign failed (exit $LASTEXITCODE) for $full" } + + & $signtool verify /pa /v $full + if ($LASTEXITCODE -ne 0) { throw "signtool verify failed (exit $LASTEXITCODE) for $full" } + Write-Info "Signed + verified: $full" + } +} +finally { + Remove-Item $metadataFile -Force -ErrorAction SilentlyContinue +} + +Write-Info "Code signing complete."