From d5437a9353b95213fe79d010ea39968c6d609903 Mon Sep 17 00:00:00 2001 From: rcourtman Date: Fri, 10 Jul 2026 23:12:12 +0100 Subject: [PATCH] Allow RCs while Windows signing is pending --- .github/workflows/build-release-candidate.yml | 57 ++++++++++++------- .github/workflows/create-release.yml | 3 +- .github/workflows/release-dry-run.yml | 3 +- ...build-release-promotion-path-2026-07-09.md | 27 +++++++++ .../subsystems/deployment-installability.md | 11 ++++ docs/releases/RELEASE_NOTES_v6.0.6-rc.1.md | 7 +++ scripts/build-release.sh | 14 +++-- .../installtests/build_release_assets_test.go | 16 ++++-- .../release_promotion_policy_test.py | 6 +- 9 files changed, 113 insertions(+), 31 deletions(-) diff --git a/.github/workflows/build-release-candidate.yml b/.github/workflows/build-release-candidate.yml index 71192ca55..928df27d3 100644 --- a/.github/workflows/build-release-candidate.yml +++ b/.github/workflows/build-release-candidate.yml @@ -7,8 +7,13 @@ on: description: 'Version number without the leading v' required: true type: string - require_platform_signing: - description: 'Require Developer ID/notarized macOS and Authenticode Windows agent binaries' + require_macos_signing: + description: 'Require Developer ID signed and notarized macOS agent binaries' + required: false + default: false + type: boolean + require_windows_signing: + description: 'Require Authenticode-signed Windows agent binaries' required: false default: false type: boolean @@ -26,7 +31,7 @@ permissions: jobs: signing-configuration: name: Verify Native Signing Configuration - if: ${{ inputs.require_platform_signing }} + if: ${{ inputs.require_macos_signing || inputs.require_windows_signing }} runs-on: ubuntu-24.04 timeout-minutes: 2 steps: @@ -40,18 +45,31 @@ jobs: APPLE_NOTARY_ISSUER_ID: ${{ secrets.APPLE_NOTARY_ISSUER_ID }} WINDOWS_CODE_SIGNING_CERTIFICATE_PFX_BASE64: ${{ secrets.WINDOWS_CODE_SIGNING_CERTIFICATE_PFX_BASE64 }} WINDOWS_CODE_SIGNING_CERTIFICATE_PASSWORD: ${{ secrets.WINDOWS_CODE_SIGNING_CERTIFICATE_PASSWORD }} + REQUIRE_MACOS_SIGNING: ${{ inputs.require_macos_signing }} + REQUIRE_WINDOWS_SIGNING: ${{ inputs.require_windows_signing }} run: | set -euo pipefail missing=0 - for name in \ - APPLE_DEVELOPER_ID_CERTIFICATE_P12_BASE64 \ - APPLE_DEVELOPER_ID_CERTIFICATE_PASSWORD \ - APPLE_DEVELOPER_ID_APPLICATION_IDENTITY \ - APPLE_NOTARY_KEY_P8_BASE64 \ - APPLE_NOTARY_KEY_ID \ - APPLE_NOTARY_ISSUER_ID \ - WINDOWS_CODE_SIGNING_CERTIFICATE_PFX_BASE64 \ - WINDOWS_CODE_SIGNING_CERTIFICATE_PASSWORD; do + required=() + if [[ "$REQUIRE_MACOS_SIGNING" == "true" ]]; then + required+=( + APPLE_DEVELOPER_ID_CERTIFICATE_P12_BASE64 + APPLE_DEVELOPER_ID_CERTIFICATE_PASSWORD + APPLE_DEVELOPER_ID_APPLICATION_IDENTITY + APPLE_NOTARY_KEY_P8_BASE64 + APPLE_NOTARY_KEY_ID + APPLE_NOTARY_ISSUER_ID + ) + fi + if [[ "$REQUIRE_WINDOWS_SIGNING" == "true" ]]; then + required+=( + WINDOWS_CODE_SIGNING_CERTIFICATE_PFX_BASE64 + WINDOWS_CODE_SIGNING_CERTIFICATE_PASSWORD + ) + else + echo "::notice::Windows Authenticode is not required for this candidate." + fi + for name in "${required[@]}"; do if [ -z "${!name:-}" ]; then echo "::error::Missing required Actions secret ${name}." missing=1 @@ -62,7 +80,7 @@ jobs: sign-macos-agent: name: Sign and Notarize macOS Agent needs: signing-configuration - if: ${{ inputs.require_platform_signing && needs.signing-configuration.result == 'success' }} + if: ${{ inputs.require_macos_signing && needs.signing-configuration.result == 'success' }} runs-on: macos-15 timeout-minutes: 30 steps: @@ -160,7 +178,7 @@ jobs: sign-windows-agent: name: Authenticode Sign Windows Agent needs: signing-configuration - if: ${{ inputs.require_platform_signing && needs.signing-configuration.result == 'success' }} + if: ${{ inputs.require_windows_signing && needs.signing-configuration.result == 'success' }} runs-on: windows-2025 timeout-minutes: 25 steps: @@ -225,7 +243,7 @@ jobs: build: name: Build and Validate Signed Candidate needs: [sign-macos-agent, sign-windows-agent] - if: ${{ always() && (!inputs.require_platform_signing || (needs.sign-macos-agent.result == 'success' && needs.sign-windows-agent.result == 'success')) }} + if: ${{ always() && (!inputs.require_macos_signing || needs.sign-macos-agent.result == 'success') && (!inputs.require_windows_signing || needs.sign-windows-agent.result == 'success') }} runs-on: ubuntu-24.04 timeout-minutes: 60 outputs: @@ -288,14 +306,14 @@ jobs: install -m 0755 "${TMP_DIR}/syft" /usr/local/bin/syft - name: Download signed macOS binaries - if: ${{ inputs.require_platform_signing }} + if: ${{ inputs.require_macos_signing }} uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 with: name: signed-macos-agent-${{ github.sha }}-${{ inputs.version }} path: native-agent-binaries - name: Download signed Windows binaries - if: ${{ inputs.require_platform_signing }} + if: ${{ inputs.require_windows_signing }} uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 with: name: signed-windows-agent-${{ github.sha }}-${{ inputs.version }} @@ -307,8 +325,9 @@ jobs: PULSE_LICENSE_PUBLIC_KEY: ${{ secrets.PULSE_LICENSE_PUBLIC_KEY }} PULSE_UPDATE_SIGNING_KEY: ${{ secrets.PULSE_UPDATE_SIGNING_KEY }} PULSE_UPDATE_SIGNING_PUBLIC_KEY: ${{ vars.PULSE_UPDATE_SIGNING_PUBLIC_KEY }} - PULSE_REQUIRE_PLATFORM_SIGNING: ${{ inputs.require_platform_signing }} - PULSE_AGENT_NATIVE_BINARIES_DIR: ${{ inputs.require_platform_signing && format('{0}/native-agent-binaries', github.workspace) || '' }} + PULSE_REQUIRE_MACOS_SIGNING: ${{ inputs.require_macos_signing }} + PULSE_REQUIRE_WINDOWS_SIGNING: ${{ inputs.require_windows_signing }} + PULSE_AGENT_NATIVE_BINARIES_DIR: ${{ (inputs.require_macos_signing || inputs.require_windows_signing) && format('{0}/native-agent-binaries', github.workspace) || '' }} - name: Validate installer signing key pins env: diff --git a/.github/workflows/create-release.yml b/.github/workflows/create-release.yml index 82bea6f1c..6de97a5f4 100644 --- a/.github/workflows/create-release.yml +++ b/.github/workflows/create-release.yml @@ -218,7 +218,8 @@ jobs: secrets: inherit with: version: ${{ needs.prepare.outputs.version }} - require_platform_signing: true + require_macos_signing: true + require_windows_signing: ${{ needs.prepare.outputs.is_prerelease != 'true' }} # Frontend checks run in parallel with backend tests frontend_checks: diff --git a/.github/workflows/release-dry-run.yml b/.github/workflows/release-dry-run.yml index cb4df8c19..4656a68fc 100644 --- a/.github/workflows/release-dry-run.yml +++ b/.github/workflows/release-dry-run.yml @@ -68,7 +68,8 @@ jobs: secrets: inherit with: version: ${{ inputs.version }} - require_platform_signing: true + require_macos_signing: true + require_windows_signing: true dry-run: name: Preflight Release Checks (No Publish) diff --git a/docs/release-control/v6/internal/records/single-build-release-promotion-path-2026-07-09.md b/docs/release-control/v6/internal/records/single-build-release-promotion-path-2026-07-09.md index e35cadaf9..c28f7ebbf 100644 --- a/docs/release-control/v6/internal/records/single-build-release-promotion-path-2026-07-09.md +++ b/docs/release-control/v6/internal/records/single-build-release-promotion-path-2026-07-09.md @@ -131,3 +131,30 @@ demo runtime unchanged. Blocked only on the absent native platform signing credentials. Repository-side orchestration, diagnostics, timeout hardening, archive validation performance, and late backend-flake containment are implemented and covered by contracts. + +## Prerelease Windows Signing Boundary (2026-07-10) + +The first `v6.0.6-rc.1` publication attempt proved Apple signing and +notarization credentials are configured, but Windows Authenticode credentials +are not. Pulse Monitoring Ltd submitted the public community project to the +SignPath Foundation open-source programme on 2026-07-10; approval and CI +integration remain externally owned and asynchronous. + +RC publication may proceed while that application is pending, provided all of +the following remain true: + +- macOS agent binaries are Developer ID signed and notarized; +- Windows agent binaries retain the exact-SHA candidate, checksums, detached + release signatures, and post-publication digest verification; +- the RC release notes state explicitly that Windows binaries are not + Authenticode-signed and may show an unknown-publisher warning; and +- stable publication continues to require successful Windows Authenticode + signing and verification. + +The reusable candidate workflow therefore separates the macOS platform-signing +requirement from the Windows Authenticode requirement. `create-release.yml` +requires Windows signing for stable versions and relaxes only that requirement +for recognized prerelease versions. `release-dry-run.yml` continues to require +both platforms so the full stable-promotion path remains rehearsed and the +`single-build-release-promotion-path` release gate remains blocked for stable +release readiness until the external signing path is proven. diff --git a/docs/release-control/v6/internal/subsystems/deployment-installability.md b/docs/release-control/v6/internal/subsystems/deployment-installability.md index 7545e4154..87183ca41 100644 --- a/docs/release-control/v6/internal/subsystems/deployment-installability.md +++ b/docs/release-control/v6/internal/subsystems/deployment-installability.md @@ -2301,6 +2301,17 @@ release-packet SBOM is absent so published RC/stable downloads can keep the updater and installer trust chain fail-closed instead of downgrading to checksum-only trust and can publish a shareable non-image software inventory alongside the signed binaries. +The immutable candidate builder must model macOS Developer ID/notarization and +Windows Authenticode as independent native-signing requirements rather than one +all-or-nothing platform switch. Governed RC publication may require signed and +notarized macOS agent binaries while Windows Authenticode approval is still an +externally owned bounded residual, but only when the RC packet explicitly +discloses the unsigned Windows publisher state and the Windows binaries retain +the exact-SHA candidate, checksum, detached-signature, and post-publication +digest controls. Stable publication and the stable-path dry-run must continue +to require both native signing lanes. `scripts/build-release.sh` must replace +only the native targets required by those independent inputs and must fail +closed when a required native-binary directory or target is absent. Historical published-release repair must flow through `scripts/backfill-release-assets.sh` and `.github/workflows/backfill-release-assets.yml` or the canonical diff --git a/docs/releases/RELEASE_NOTES_v6.0.6-rc.1.md b/docs/releases/RELEASE_NOTES_v6.0.6-rc.1.md index ba9cc6de1..fa62457ce 100644 --- a/docs/releases/RELEASE_NOTES_v6.0.6-rc.1.md +++ b/docs/releases/RELEASE_NOTES_v6.0.6-rc.1.md @@ -44,6 +44,8 @@ behavior, and broad security and reliability hardening. fail fast when signing configuration is incomplete. - Docker updates now recreate the container instead of attempting a restart that cannot apply a new image. +- Docker containers retain their grouped-by-host view and open configured web + links consistently after REST resource snapshot hydration. - Docker and Kubernetes agents tolerate realistic clock skew when evaluating liveness, and posture alerts no longer ignore the intended guest-suppression rules. @@ -71,5 +73,10 @@ Pulse Mobile candidate builds with runtime version 1 receive the matching typed-action approval client through the candidate OTA channel; no public store rollout is part of this RC. +Windows Unified Agent binaries in this release candidate retain the same +checksum and detached-signature verification used by `v6.0.5`, but they are +not yet Authenticode-signed and Windows may show an unknown-publisher warning. +Public Windows Authenticode signing remains required before stable promotion. + Paid Pulse Pro, Relay, and eligible legacy customers should continue to use the private download page and private runtime image for paid runtime features. diff --git a/scripts/build-release.sh b/scripts/build-release.sh index 5d365468d..bb5cc0fa3 100755 --- a/scripts/build-release.sh +++ b/scripts/build-release.sh @@ -152,7 +152,13 @@ done # so the immutable candidate manifest covers the exact signed bytes users get. if [[ -n "${PULSE_AGENT_NATIVE_BINARIES_DIR:-}" ]]; then native_dir="${PULSE_AGENT_NATIVE_BINARIES_DIR}" - native_targets=(darwin-amd64 darwin-arm64 windows-amd64 windows-arm64 windows-386) + native_targets=() + if [[ "${PULSE_REQUIRE_MACOS_SIGNING:-false}" == "true" ]]; then + native_targets+=(darwin-amd64 darwin-arm64) + fi + if [[ "${PULSE_REQUIRE_WINDOWS_SIGNING:-false}" == "true" ]]; then + native_targets+=(windows-amd64 windows-arm64 windows-386) + fi for target in "${native_targets[@]}"; do filename="pulse-agent-${target}" if [[ "$target" == windows-* ]]; then @@ -164,9 +170,9 @@ if [[ -n "${PULSE_AGENT_NATIVE_BINARIES_DIR:-}" ]]; then fi cp "${native_dir}/${filename}" "${BUILD_DIR}/${filename}" done - echo "Applied platform-native signed Unified Agent binaries." -elif [[ "${PULSE_REQUIRE_PLATFORM_SIGNING:-false}" == "true" ]]; then - echo "Error: platform signing is required but PULSE_AGENT_NATIVE_BINARIES_DIR is empty." >&2 + echo "Applied required platform-native signed Unified Agent binaries." +elif [[ "${PULSE_REQUIRE_MACOS_SIGNING:-false}" == "true" || "${PULSE_REQUIRE_WINDOWS_SIGNING:-false}" == "true" ]]; then + echo "Error: required native signing is enabled but PULSE_AGENT_NATIVE_BINARIES_DIR is empty." >&2 exit 1 fi diff --git a/scripts/installtests/build_release_assets_test.go b/scripts/installtests/build_release_assets_test.go index 8c8d6e2b0..7123731da 100644 --- a/scripts/installtests/build_release_assets_test.go +++ b/scripts/installtests/build_release_assets_test.go @@ -709,7 +709,8 @@ func TestAgentRuntimeImagePersistsAgentIdentityByDefault(t *testing.T) { func TestReleaseCandidateRequiresPlatformNativeAgentSigning(t *testing.T) { candidateWorkflowPath := repoFile(".github", "workflows", "build-release-candidate.yml") assertFileContainsAll(t, candidateWorkflowPath, - `require_platform_signing:`, + `require_macos_signing:`, + `require_windows_signing:`, `sign-macos-agent:`, `codesign --force --timestamp --options runtime`, `xcrun notarytool submit`, @@ -729,13 +730,18 @@ func TestReleaseCandidateRequiresPlatformNativeAgentSigning(t *testing.T) { t.Fatal("bare command-line Mach-O binaries must not use Gatekeeper app assessment after notarization") } assertFileContainsAll(t, repoFile(".github", "workflows", "create-release.yml"), - `require_platform_signing: true`, + `require_macos_signing: true`, + `require_windows_signing: ${{ needs.prepare.outputs.is_prerelease != 'true' }}`, ) assertFileContainsAll(t, repoFile("scripts", "build-release.sh"), `PULSE_AGENT_NATIVE_BINARIES_DIR`, - `native_targets=(darwin-amd64 darwin-arm64 windows-amd64 windows-arm64 windows-386)`, - `Applied platform-native signed Unified Agent binaries.`, - `platform signing is required but PULSE_AGENT_NATIVE_BINARIES_DIR is empty.`, + `native_targets=()`, + `PULSE_REQUIRE_MACOS_SIGNING:-false`, + `native_targets+=(darwin-amd64 darwin-arm64)`, + `PULSE_REQUIRE_WINDOWS_SIGNING:-false`, + `native_targets+=(windows-amd64 windows-arm64 windows-386)`, + `Applied required platform-native signed Unified Agent binaries.`, + `required native signing is enabled but PULSE_AGENT_NATIVE_BINARIES_DIR is empty.`, ) } diff --git a/scripts/release_control/release_promotion_policy_test.py b/scripts/release_control/release_promotion_policy_test.py index 9911791db..3aa19b2f2 100644 --- a/scripts/release_control/release_promotion_policy_test.py +++ b/scripts/release_control/release_promotion_policy_test.py @@ -486,7 +486,8 @@ class ReleasePromotionPolicyTest(unittest.TestCase): self.assertIn("rc-to-ga-rehearsal-summary", workflow) self.assertIn("build_release_candidate:", workflow) self.assertIn("if: ${{ inputs.version != '' }}", workflow) - self.assertIn("require_platform_signing: true", workflow) + self.assertIn("require_macos_signing: true", workflow) + self.assertIn("require_windows_signing: true", workflow) self.assertNotIn("if: ${{ github.event_name == 'workflow_dispatch' }}", workflow) self.assertIn("record_rc_to_ga_rehearsal.py --run-id ${{ github.run_id }}", workflow) self.assertIn("rc-to-ga-promotion-readiness-rehearsal-.md", workflow) @@ -623,6 +624,9 @@ class ReleasePromotionPolicyTest(unittest.TestCase): self.assertIn("timeout-minutes: 60", candidate_workflow) self.assertIn("Verify Native Signing Configuration", candidate_workflow) self.assertEqual(candidate_workflow.count("needs: signing-configuration"), 2) + self.assertIn("require_windows_signing: ${{ needs.prepare.outputs.is_prerelease != 'true' }}", content) + self.assertIn('if [[ "$REQUIRE_WINDOWS_SIGNING" == "true" ]]', candidate_workflow) + self.assertIn("inputs.require_windows_signing", candidate_workflow) for signing_secret in ( "APPLE_DEVELOPER_ID_CERTIFICATE_P12_BASE64", "APPLE_DEVELOPER_ID_CERTIFICATE_PASSWORD",