Files
DeepSeek-Reasonix/scripts/complete-signpath-request.ps1
T
yhh 55419043c3 feat(release): the studio line signs its Windows artifacts through SignPath
Studio's Windows artifacts were unsigned by construction — the production
SignPath policy trusted exactly one origin, protected main-v2, so a build from
this branch could not request the certificate. The workflow said so and left it
there. On a managed Windows endpoint that is not a SmartScreen warning: the
installer is refused at CreateProcess, and Windows reports it as "cannot access
the specified device, path, or file", which reads as a permission problem and
is not one.

The desktop line's mechanism comes over whole, adapted to what Studio ships.
Signing is two-staged for the reason main-v2 states: signing only the finished
NSIS container leaves reasonix-studio.exe unsigned on disk after installation,
which is precisely what reputation and ML heuristics quarantine. So the
executable is signed while it is still a loose file, the packages are rebuilt
around those exact bytes, and the container is signed second — with the
artifact configuration verifying the payload's signature rather than trusting
this workflow's ordering.

studio-build.sh grows the seam that makes that possible: --windows-payload
builds what a user runs and stops, --windows-package builds the zip and the
installer from a payload directory it is handed. The one-shot platform path
composes the two, so an unsigned build and a maintainer's local run are byte
-identical to what they were.

.signpath/ carries this line's own contract — branch studio, workflow
release-studio.yml — and cmd/signpath-contract validates and fingerprints it
before anything is built, so a widened origin fails at the gate rather than at
the signing request. Its branch pin is studio rather than read from the file it
checks: a guard that reads its own answer out of its subject cannot guard.

Signing engages only when SIGNPATH_API_TOKEN is set, mirroring the APPLE_* gate
— forks and token-less runs still build, unsigned.

This cannot take effect on the repository side alone: the release-signing policy
in SignPath's console still trusts only main-v2, and until the studio origin is
added there every request from this workflow is rejected. See the PR body.
2026-08-26 22:18:42 -07:00

162 lines
4.9 KiB
PowerShell

[CmdletBinding()]
param(
[Parameter(Mandatory = $true)]
[ValidatePattern("^[0-9a-fA-F-]{36}$")]
[string]$OrganizationId,
[Parameter(Mandatory = $true)]
[ValidatePattern("^[0-9a-fA-F-]{36}$")]
[string]$SigningRequestId,
[Parameter(Mandatory = $true)]
[ValidateNotNullOrEmpty()]
[string]$ExpectedSigningPolicySlug,
[Parameter(Mandatory = $true)]
[ValidateNotNullOrEmpty()]
[string]$OutputArtifactDirectory,
[ValidateNotNullOrEmpty()]
[string]$ApiUrl = "https://app.signpath.io/api",
[ValidateRange(30, 7200)]
[int]$TimeoutSeconds = 1800,
[ValidateRange(1, 60)]
[int]$PollIntervalSeconds = 5,
[switch]$WaitForExternalApproval
)
Set-StrictMode -Version Latest
$ErrorActionPreference = "Stop"
$ProgressPreference = "SilentlyContinue"
$apiToken = $env:SIGNPATH_API_TOKEN
if ([string]::IsNullOrWhiteSpace($apiToken)) {
throw "SIGNPATH_API_TOKEN is required to approve and download a SignPath request."
}
$parsedApiUrl = $null
if (
-not [Uri]::TryCreate($ApiUrl, [UriKind]::Absolute, [ref]$parsedApiUrl) -or
$parsedApiUrl.Scheme -ne "https"
) {
throw "ApiUrl must be an absolute HTTPS URL."
}
$workspace = if ([string]::IsNullOrWhiteSpace($env:GITHUB_WORKSPACE)) {
[IO.Path]::GetFullPath((Get-Location).Path)
} else {
[IO.Path]::GetFullPath($env:GITHUB_WORKSPACE)
}
$outputDirectory = if ([IO.Path]::IsPathRooted($OutputArtifactDirectory)) {
[IO.Path]::GetFullPath($OutputArtifactDirectory)
} else {
[IO.Path]::GetFullPath((Join-Path $workspace $OutputArtifactDirectory))
}
$workspacePrefix = $workspace.TrimEnd(
[IO.Path]::DirectorySeparatorChar,
[IO.Path]::AltDirectorySeparatorChar
) + [IO.Path]::DirectorySeparatorChar
if (-not $outputDirectory.StartsWith($workspacePrefix, [StringComparison]::OrdinalIgnoreCase)) {
throw "OutputArtifactDirectory must resolve inside GITHUB_WORKSPACE."
}
$headers = @{
Authorization = "Bearer $apiToken"
}
$requestBaseUrl = (
$ApiUrl.TrimEnd("/") +
"/v1/$OrganizationId/SigningRequests/$SigningRequestId"
)
$request = Invoke-RestMethod `
-Method Get `
-Uri $requestBaseUrl `
-Headers $headers
if ($request.signingPolicySlug -ne $ExpectedSigningPolicySlug) {
throw (
"SignPath request policy mismatch: got '$($request.signingPolicySlug)', " +
"expected '$ExpectedSigningPolicySlug'."
)
}
$deadline = [DateTimeOffset]::UtcNow.AddSeconds($TimeoutSeconds)
$approvalSubmitted = $false
$externalApprovalNoticeWritten = $false
while ($true) {
$status = Invoke-RestMethod `
-Method Get `
-Uri "$requestBaseUrl/Status" `
-Headers $headers
Write-Host (
"SignPath request $SigningRequestId status: " +
"$($status.status) ($($status.workflowStatus))"
)
if ($status.isFinalStatus) {
if ($status.status -ne "Completed") {
throw (
"SignPath request ended with status '$($status.status)' " +
"and workflow status '$($status.workflowStatus)'."
)
}
break
}
if ($status.status -eq "WaitingForApproval" -and -not $approvalSubmitted) {
if ($WaitForExternalApproval) {
if (-not $externalApprovalNoticeWritten) {
Write-Host (
"Waiting for an authorized SignPath user to approve request " +
"$SigningRequestId."
)
$externalApprovalNoticeWritten = $true
}
} else {
Invoke-RestMethod `
-Method Post `
-Uri "$requestBaseUrl/Approve" `
-Headers $headers | Out-Null
$approvalSubmitted = $true
Write-Host "Approved SignPath request $SigningRequestId through the release CI identity."
}
}
if ([DateTimeOffset]::UtcNow -ge $deadline) {
throw "Timed out waiting for SignPath request $SigningRequestId."
}
Start-Sleep -Seconds $PollIntervalSeconds
}
$temporaryRoot = if ([string]::IsNullOrWhiteSpace($env:RUNNER_TEMP)) {
[IO.Path]::GetTempPath()
} else {
$env:RUNNER_TEMP
}
$temporaryArchive = Join-Path $temporaryRoot "signpath-$SigningRequestId.zip"
try {
Invoke-WebRequest `
-Method Get `
-Uri "$requestBaseUrl/SignedArtifact" `
-Headers $headers `
-OutFile $temporaryArchive
if (-not (Test-Path -LiteralPath $temporaryArchive -PathType Leaf)) {
throw "SignPath did not return a signed artifact."
}
if ((Get-Item -LiteralPath $temporaryArchive).Length -eq 0) {
throw "SignPath returned an empty signed artifact."
}
New-Item -ItemType Directory -Force -Path $outputDirectory | Out-Null
Expand-Archive `
-LiteralPath $temporaryArchive `
-DestinationPath $outputDirectory `
-Force
} finally {
Remove-Item -LiteralPath $temporaryArchive -Force -ErrorAction SilentlyContinue
}
Write-Host "Downloaded and extracted signed artifact to $outputDirectory."