mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-09-10 02:25:56 +00:00
ci(signing): add isolated SignPath test proof
This commit is contained in:
@@ -0,0 +1,237 @@
|
||||
name: SignPath Test Signing Proof
|
||||
run-name: SignPath test-signing proof for v${{ inputs.version }}
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
version:
|
||||
description: 'Version label embedded in the test binaries (without leading v)'
|
||||
required: true
|
||||
type: string
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
concurrency:
|
||||
group: signpath-test-signing-proof
|
||||
cancel-in-progress: false
|
||||
|
||||
jobs:
|
||||
test-signing-proof:
|
||||
name: SignPath Test Signing Proof (Never Publish)
|
||||
runs-on: windows-2025
|
||||
timeout-minutes: 50
|
||||
permissions:
|
||||
actions: read
|
||||
contents: read
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
|
||||
with:
|
||||
go-version-file: go.mod
|
||||
cache: true
|
||||
|
||||
- name: Verify isolated test-signing configuration
|
||||
shell: pwsh
|
||||
env:
|
||||
SIGNPATH_API_TOKEN: ${{ secrets.SIGNPATH_API_TOKEN }}
|
||||
SIGNPATH_ORGANIZATION_ID: ${{ vars.SIGNPATH_ORGANIZATION_ID }}
|
||||
SIGNPATH_PROJECT_SLUG: ${{ vars.SIGNPATH_PROJECT_SLUG }}
|
||||
SIGNPATH_ARTIFACT_CONFIGURATION_SLUG: ${{ vars.SIGNPATH_ARTIFACT_CONFIGURATION_SLUG }}
|
||||
SIGNPATH_RELEASE_POLICY_SLUG: ${{ vars.SIGNPATH_SIGNING_POLICY_SLUG }}
|
||||
TEST_VERSION: ${{ inputs.version }}
|
||||
run: |
|
||||
$ErrorActionPreference = 'Stop'
|
||||
if ($env:GITHUB_REF_NAME -ne 'main') {
|
||||
throw "SignPath test proof must run from main, not $env:GITHUB_REF_NAME."
|
||||
}
|
||||
foreach ($name in @(
|
||||
'SIGNPATH_API_TOKEN',
|
||||
'SIGNPATH_ORGANIZATION_ID',
|
||||
'SIGNPATH_PROJECT_SLUG',
|
||||
'SIGNPATH_ARTIFACT_CONFIGURATION_SLUG'
|
||||
)) {
|
||||
if ([string]::IsNullOrWhiteSpace((Get-Item "Env:$name").Value)) {
|
||||
throw "Missing required $name."
|
||||
}
|
||||
}
|
||||
if ($env:SIGNPATH_RELEASE_POLICY_SLUG -ne 'release-signing') {
|
||||
throw 'The canonical SIGNPATH_SIGNING_POLICY_SLUG must remain release-signing.'
|
||||
}
|
||||
if ($env:TEST_VERSION -notmatch '^6\.[0-9]+\.[0-9]+(?:-(?:rc|alpha|beta)\.[0-9]+)?$') {
|
||||
throw 'Version must be a valid Pulse v6 release or prerelease version without a leading v.'
|
||||
}
|
||||
$repositoryVersion = (Get-Content VERSION -Raw).Trim()
|
||||
if ($env:TEST_VERSION -ne $repositoryVersion) {
|
||||
throw "Test version $env:TEST_VERSION does not match repository VERSION $repositoryVersion."
|
||||
}
|
||||
|
||||
- name: Build unsigned Windows agent binaries
|
||||
shell: pwsh
|
||||
env:
|
||||
PULSE_UPDATE_SIGNING_PUBLIC_KEY: ${{ vars.PULSE_UPDATE_SIGNING_PUBLIC_KEY }}
|
||||
TEST_VERSION: ${{ inputs.version }}
|
||||
run: |
|
||||
$ErrorActionPreference = 'Stop'
|
||||
if ([string]::IsNullOrWhiteSpace($env:PULSE_UPDATE_SIGNING_PUBLIC_KEY)) {
|
||||
throw 'Missing required PULSE_UPDATE_SIGNING_PUBLIC_KEY.'
|
||||
}
|
||||
New-Item -ItemType Directory -Path unsigned-native-agent-binaries -Force | Out-Null
|
||||
$ldflags = & bash ./scripts/release_ldflags.sh agent --version "v$env:TEST_VERSION" --update-public-keys $env:PULSE_UPDATE_SIGNING_PUBLIC_KEY
|
||||
foreach ($arch in @('amd64', 'arm64', '386')) {
|
||||
$env:GOOS = 'windows'
|
||||
$env:GOARCH = $arch
|
||||
$env:CGO_ENABLED = '0'
|
||||
$output = "unsigned-native-agent-binaries/pulse-agent-windows-$arch.exe"
|
||||
go build -buildvcs=false -trimpath -ldflags="$ldflags" -o $output ./cmd/pulse-agent
|
||||
if ($LASTEXITCODE -ne 0) { throw "Go build failed for Windows $arch." }
|
||||
}
|
||||
|
||||
- name: Upload unsigned SignPath test input
|
||||
id: upload-unsigned-windows
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: unsigned-signpath-test-${{ github.sha }}-${{ inputs.version }}
|
||||
path: unsigned-native-agent-binaries/
|
||||
if-no-files-found: error
|
||||
retention-days: 1
|
||||
compression-level: 0
|
||||
|
||||
- name: Submit SignPath test-signing request
|
||||
id: signpath
|
||||
uses: signpath/github-action-submit-signing-request@b9d91eadd323de506c0c81cf0c7fe7438f3360fd # v2
|
||||
with:
|
||||
api-token: ${{ secrets.SIGNPATH_API_TOKEN }}
|
||||
organization-id: ${{ vars.SIGNPATH_ORGANIZATION_ID }}
|
||||
project-slug: ${{ vars.SIGNPATH_PROJECT_SLUG }}
|
||||
signing-policy-slug: test-signing
|
||||
artifact-configuration-slug: ${{ vars.SIGNPATH_ARTIFACT_CONFIGURATION_SLUG }}
|
||||
github-artifact-id: ${{ steps.upload-unsigned-windows.outputs.artifact-id }}
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
wait-for-completion: true
|
||||
wait-for-completion-timeout-in-seconds: 2400
|
||||
output-artifact-directory: signpath-test-output
|
||||
parameters: |
|
||||
version: ${{ toJSON(inputs.version) }}
|
||||
|
||||
- name: Verify test signatures and write non-production evidence
|
||||
shell: pwsh
|
||||
env:
|
||||
SIGNPATH_SIGNING_REQUEST_ID: ${{ steps.signpath.outputs.signing-request-id }}
|
||||
SIGNPATH_SIGNING_REQUEST_URL: ${{ steps.signpath.outputs.signing-request-web-url }}
|
||||
SIGNPATH_INPUT_ARTIFACT_ID: ${{ steps.upload-unsigned-windows.outputs.artifact-id }}
|
||||
SIGNPATH_ARTIFACT_CONFIGURATION_SLUG: ${{ vars.SIGNPATH_ARTIFACT_CONFIGURATION_SLUG }}
|
||||
TEST_VERSION: ${{ inputs.version }}
|
||||
run: |
|
||||
$ErrorActionPreference = 'Stop'
|
||||
$sourceDir = (Resolve-Path 'signpath-test-output').Path
|
||||
$expectedNames = @(
|
||||
'pulse-agent-windows-386.exe',
|
||||
'pulse-agent-windows-amd64.exe',
|
||||
'pulse-agent-windows-arm64.exe'
|
||||
)
|
||||
$actualNames = @(
|
||||
Get-ChildItem $sourceDir -File -Recurse |
|
||||
ForEach-Object { [IO.Path]::GetRelativePath($sourceDir, $_.FullName).Replace('\', '/') } |
|
||||
Sort-Object
|
||||
)
|
||||
if (Compare-Object $expectedNames $actualNames) {
|
||||
throw "SignPath returned an unexpected file set: $($actualNames -join ', ')."
|
||||
}
|
||||
|
||||
$probePath = Join-Path $sourceDir $expectedNames[0]
|
||||
$probeSignature = Get-AuthenticodeSignature $probePath
|
||||
if ($null -eq $probeSignature.SignerCertificate) {
|
||||
throw 'The SignPath test output does not contain an Authenticode signer certificate.'
|
||||
}
|
||||
|
||||
$certificate = $probeSignature.SignerCertificate
|
||||
$thumbprint = $certificate.Thumbprint
|
||||
$certificatePath = Join-Path $env:RUNNER_TEMP 'signpath-test-signer.cer'
|
||||
[IO.File]::WriteAllBytes(
|
||||
$certificatePath,
|
||||
$certificate.Export([Security.Cryptography.X509Certificates.X509ContentType]::Cert)
|
||||
)
|
||||
|
||||
Import-Certificate -FilePath $certificatePath -CertStoreLocation Cert:\CurrentUser\Root | Out-Null
|
||||
Import-Certificate -FilePath $certificatePath -CertStoreLocation Cert:\CurrentUser\TrustedPublisher | Out-Null
|
||||
try {
|
||||
$signtool = Get-ChildItem "${env:ProgramFiles(x86)}\Windows Kits\10\bin" -Filter signtool.exe -Recurse |
|
||||
Sort-Object FullName -Descending |
|
||||
Select-Object -First 1 -ExpandProperty FullName
|
||||
if ([string]::IsNullOrWhiteSpace($signtool)) {
|
||||
throw 'signtool.exe was not found on the Windows runner.'
|
||||
}
|
||||
|
||||
$files = @()
|
||||
foreach ($name in $expectedNames) {
|
||||
$path = Join-Path $sourceDir $name
|
||||
& $signtool verify /pa /v $path
|
||||
if ($LASTEXITCODE -ne 0) { throw "Authenticode verification failed for $name." }
|
||||
$signature = Get-AuthenticodeSignature $path
|
||||
if ($signature.Status -ne 'Valid' -or $null -eq $signature.SignerCertificate) {
|
||||
throw "Invalid Authenticode status for ${name}: $($signature.Status)."
|
||||
}
|
||||
if ($signature.SignerCertificate.Thumbprint -ne $thumbprint) {
|
||||
throw "SignPath test output used inconsistent signer certificates for $name."
|
||||
}
|
||||
$files += [ordered]@{
|
||||
name = $name
|
||||
sha256 = (Get-FileHash $path -Algorithm SHA256).Hash.ToLowerInvariant()
|
||||
}
|
||||
}
|
||||
|
||||
$evidence = [ordered]@{
|
||||
schemaVersion = 1
|
||||
nonProduction = $true
|
||||
signedArtifactsPublished = $false
|
||||
signedArtifactsUploadedAsGitHubArtifact = $false
|
||||
signingPolicy = 'test-signing'
|
||||
artifactConfiguration = $env:SIGNPATH_ARTIFACT_CONFIGURATION_SLUG
|
||||
version = $env:TEST_VERSION
|
||||
sourceSha = $env:GITHUB_SHA
|
||||
sourceRef = $env:GITHUB_REF
|
||||
workflowRunUrl = "https://github.com/$env:GITHUB_REPOSITORY/actions/runs/$env:GITHUB_RUN_ID"
|
||||
signPathSigningRequestId = $env:SIGNPATH_SIGNING_REQUEST_ID
|
||||
signPathSigningRequestUrl = $env:SIGNPATH_SIGNING_REQUEST_URL
|
||||
githubInputArtifactId = $env:SIGNPATH_INPUT_ARTIFACT_ID
|
||||
signer = [ordered]@{
|
||||
subject = $certificate.Subject
|
||||
issuer = $certificate.Issuer
|
||||
thumbprint = $thumbprint
|
||||
notBefore = $certificate.NotBefore.ToUniversalTime().ToString('o')
|
||||
notAfter = $certificate.NotAfter.ToUniversalTime().ToString('o')
|
||||
}
|
||||
files = $files
|
||||
}
|
||||
$evidence | ConvertTo-Json -Depth 6 | Set-Content signpath-test-signing-evidence.json -Encoding utf8NoBOM
|
||||
}
|
||||
finally {
|
||||
Remove-Item "Cert:\CurrentUser\Root\$thumbprint" -Force -ErrorAction SilentlyContinue
|
||||
Remove-Item "Cert:\CurrentUser\TrustedPublisher\$thumbprint" -Force -ErrorAction SilentlyContinue
|
||||
}
|
||||
|
||||
- name: Upload non-production evidence
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: signpath-test-signing-evidence-${{ github.sha }}-${{ inputs.version }}
|
||||
path: signpath-test-signing-evidence.json
|
||||
if-no-files-found: error
|
||||
retention-days: 30
|
||||
|
||||
- name: Record non-production boundary
|
||||
shell: pwsh
|
||||
run: |
|
||||
@"
|
||||
### SignPath test-signing proof
|
||||
|
||||
- Source: ``$env:GITHUB_SHA`` on ``main``
|
||||
- Policy: ``test-signing``
|
||||
- Result: signature verification passed for the three expected Windows agents
|
||||
- Publication: test-signed binaries were not uploaded as a GitHub artifact or published
|
||||
|
||||
The test certificate is untrusted and this run is not production-signing evidence.
|
||||
"@ | Add-Content $env:GITHUB_STEP_SUMMARY
|
||||
@@ -501,6 +501,43 @@ class ReleasePromotionPolicyTest(unittest.TestCase):
|
||||
self.assertNotIn("docs/releases/RELEASE_NOTES_v6.md", demo_copy)
|
||||
self.assertNotIn("docs/releases/V6_RC_OPERATOR_SUPPORT_PACK.md", demo_copy)
|
||||
|
||||
def test_signpath_test_signing_workflow_is_non_publishing_and_test_only(self) -> None:
|
||||
workflow = read(".github/workflows/signpath-test-signing.yml")
|
||||
|
||||
self.assertIn("workflow_dispatch:", workflow)
|
||||
self.assertNotIn("pull_request:", workflow)
|
||||
self.assertNotIn("schedule:", workflow)
|
||||
self.assertNotIn("push:", workflow)
|
||||
self.assertIn("SignPath Test Signing Proof (Never Publish)", workflow)
|
||||
self.assertIn("if ($env:GITHUB_REF_NAME -ne 'main')", workflow)
|
||||
self.assertNotIn("'${{ inputs.version }}'", workflow)
|
||||
self.assertIn("does not match repository VERSION", workflow)
|
||||
self.assertIn("signing-policy-slug: test-signing", workflow)
|
||||
self.assertNotIn(
|
||||
"signing-policy-slug: ${{ vars.SIGNPATH_SIGNING_POLICY_SLUG }}",
|
||||
workflow,
|
||||
)
|
||||
self.assertIn(
|
||||
"The canonical SIGNPATH_SIGNING_POLICY_SLUG must remain release-signing.",
|
||||
workflow,
|
||||
)
|
||||
self.assertIn(
|
||||
"signpath/github-action-submit-signing-request@b9d91eadd323de506c0c81cf0c7fe7438f3360fd # v2",
|
||||
workflow,
|
||||
)
|
||||
self.assertIn("signedArtifactsPublished = $false", workflow)
|
||||
self.assertIn("signedArtifactsUploadedAsGitHubArtifact = $false", workflow)
|
||||
self.assertIn("nonProduction = $true", workflow)
|
||||
self.assertIn("path: signpath-test-signing-evidence.json", workflow)
|
||||
self.assertNotIn("gh release", workflow)
|
||||
self.assertNotIn("release-candidate", workflow)
|
||||
for name in (
|
||||
"pulse-agent-windows-amd64.exe",
|
||||
"pulse-agent-windows-arm64.exe",
|
||||
"pulse-agent-windows-386.exe",
|
||||
):
|
||||
self.assertIn(name, workflow)
|
||||
|
||||
def test_update_demo_server_workflow_uses_stable_tag_example(self) -> None:
|
||||
workflow = read(".github/workflows/update-demo-server.yml")
|
||||
self.assertIn("Stable release tag to deploy (e.g., v6.0.0)", workflow)
|
||||
|
||||
Reference in New Issue
Block a user