diff --git a/.github/workflows/create-release.yml b/.github/workflows/create-release.yml index 77cdf44626..fc75508523 100644 --- a/.github/workflows/create-release.yml +++ b/.github/workflows/create-release.yml @@ -38,12 +38,12 @@ on: required: false type: string unsigned_windows_exception: - description: 'v6.1.0, v6.1.1, v6.1.2, v6.2.0, v6.2.1, or v6.3.0 only: publish Windows agents without Authenticode under a recorded owner exception' + description: 'v6.1.0, v6.1.1, v6.1.2, v6.2.0, v6.2.1, v6.3.0, or v6.3.1 only: publish Windows agents without Authenticode under a recorded owner exception' required: false type: boolean default: false unsigned_windows_reason: - description: 'v6.1.0, v6.1.1, v6.1.2, v6.2.0, v6.2.1, or v6.3.0 only: owner reason for the unsigned Windows exception' + description: 'v6.1.0, v6.1.1, v6.1.2, v6.2.0, v6.2.1, v6.3.0, or v6.3.1 only: owner reason for the unsigned Windows exception' required: false type: string historical_asset_backfill_only: diff --git a/.github/workflows/release-dry-run.yml b/.github/workflows/release-dry-run.yml index c53ad8f92d..58fb585769 100644 --- a/.github/workflows/release-dry-run.yml +++ b/.github/workflows/release-dry-run.yml @@ -43,12 +43,12 @@ on: required: false type: string unsigned_windows_exception: - description: 'v6.1.0, v6.1.1, v6.1.2, v6.2.0, v6.2.1, or v6.3.0 only: rehearse Windows agents without Authenticode under a recorded owner exception' + description: 'v6.1.0, v6.1.1, v6.1.2, v6.2.0, v6.2.1, v6.3.0, or v6.3.1 only: rehearse Windows agents without Authenticode under a recorded owner exception' required: false type: boolean default: false unsigned_windows_reason: - description: 'v6.1.0, v6.1.1, v6.1.2, v6.2.0, v6.2.1, or v6.3.0 only: owner reason for the unsigned Windows exception' + description: 'v6.1.0, v6.1.1, v6.1.2, v6.2.0, v6.2.1, v6.3.0, or v6.3.1 only: owner reason for the unsigned Windows exception' required: false type: string note: @@ -79,7 +79,7 @@ jobs: with: version: ${{ inputs.version }} require_macos_signing: true - require_windows_signing: ${{ !contains(inputs.version, '-') && !((inputs.version == '6.1.0' || inputs.version == '6.1.1' || inputs.version == '6.1.2' || inputs.version == '6.2.0' || inputs.version == '6.2.1' || inputs.version == '6.3.0') && inputs.unsigned_windows_exception) }} + require_windows_signing: ${{ !contains(inputs.version, '-') && !((inputs.version == '6.1.0' || inputs.version == '6.1.1' || inputs.version == '6.1.2' || inputs.version == '6.2.0' || inputs.version == '6.2.1' || inputs.version == '6.3.0' || inputs.version == '6.3.1') && inputs.unsigned_windows_exception) }} windows_signing_backend: signpath dry-run: diff --git a/docs/release-control/v6/internal/PRE_RELEASE_CHECKLIST.md b/docs/release-control/v6/internal/PRE_RELEASE_CHECKLIST.md index cfae3838e7..cb3615b5e1 100644 --- a/docs/release-control/v6/internal/PRE_RELEASE_CHECKLIST.md +++ b/docs/release-control/v6/internal/PRE_RELEASE_CHECKLIST.md @@ -50,6 +50,11 @@ Use this as the final gate before cutting a Pulse v6 pre-release. packet must retain exact-SHA, checksum, detached-signature, immutable- manifest, and published-digest proof and publicly disclose the Unknown Publisher warning. +- On 2026-08-23 the release owner approved the same bounded integrity path for + `v6.3.1` after an exact-SHA signing rehearsal proved that SignPath's + production certificate remains CSR pending and the `release-signing` policy + is therefore invalid. This exception applies only to `v6.3.1` and retains + mandatory public Unknown Publisher disclosure. - `known-rc-issue-closure-for-ga` was introduced on 2026-04-21 to reflect the locked rule that v6 GA must be feature-complete relative to the prerelease issue set. It is currently blocked on the dated RC issue-closure packet. diff --git a/docs/release-control/v6/internal/RELEASE_PROMOTION_POLICY.md b/docs/release-control/v6/internal/RELEASE_PROMOTION_POLICY.md index 6d9f1ddcf6..ab1f0c9fd9 100644 --- a/docs/release-control/v6/internal/RELEASE_PROMOTION_POLICY.md +++ b/docs/release-control/v6/internal/RELEASE_PROMOTION_POLICY.md @@ -433,6 +433,22 @@ TLS-unverified receipts leave the claim at `implemented` or signatures, and published-digest verification. Public notes must disclose that the binaries are not Authenticode-signed and may display an Unknown Publisher warning. +15. v6.3.1 unsigned-Windows exception: + - On 2026-08-23, the release owner explicitly authorized unsigned Windows + Unified Agent artifacts for stable `v6.3.1` after exact-SHA rehearsal + `32634435531` proved that the SignPath production certificate remains + `CSR PENDING` and the `release-signing` policy is invalid. The dated + decision record is + `docs/release-control/v6/internal/records/v6.3.1-unsigned-windows-owner-approval-2026-08-23.md`. + - This is a bounded `v6.3.1` decision, not evidence that Authenticode + succeeded and not a standing exception. Stable `v6.3.2` and later restore + mandatory Authenticode unless another explicit, version-bound owner + decision is recorded. + - Windows artifacts remain bound by the exact release SHA, immutable + candidate manifest, SHA-256 checksums, detached `.sig` and `.sshsig` + signatures, and published-digest verification. Public notes must disclose + that the binaries are not Authenticode-signed and may display an Unknown + Publisher warning. ## Single-Build Release Path diff --git a/docs/release-control/v6/internal/records/v6.3.1-unsigned-windows-owner-approval-2026-08-23.md b/docs/release-control/v6/internal/records/v6.3.1-unsigned-windows-owner-approval-2026-08-23.md new file mode 100644 index 0000000000..6b13a28a67 --- /dev/null +++ b/docs/release-control/v6/internal/records/v6.3.1-unsigned-windows-owner-approval-2026-08-23.md @@ -0,0 +1,39 @@ +# v6.3.1 Unsigned Windows Owner Approval + +- Decision date: `2026-08-23` +- Release: `v6.3.1` +- Promotion path: emergency stable patch from `main` +- Rollback target: `v6.3.0` +- Exact rollback reinstall command: `./scripts/install.sh --version v6.3.0` +- Failed signing rehearsal: `32634435531` + +## Owner Decision + +Exact-SHA rehearsal `32634435531` reached the SignPath production +`release-signing` policy after the GitHub-hosted provenance correction. SignPath +rejected the request because the SignPath production certificate remains `CSR PENDING`, +which leaves the production policy invalid. The SignPath project, repository, +branch, build definitions, submitter, and approval configuration are present; +certificate issuance is not an owner-actionable step in the SignPath workspace. + +On 2026-08-23, the release owner explicitly authorized unsigned Windows Unified +Agent artifacts for stable `v6.3.1` so the externally pending certificate does +not block the otherwise qualified emergency patch. + +This is a version-bound risk acceptance, not evidence that Authenticode +succeeded and not a standing exception for later releases. Stable `v6.3.2` and +later restore mandatory Authenticode unless a new explicit, version-bound owner +decision is recorded. + +## Required Integrity Controls + +The exception changes only the Authenticode requirement. Windows artifacts must +still be built from the exact release SHA and bound to the immutable candidate +manifest, SHA-256 checksums, detached `.sig` and `.sshsig` signatures, and +published-digest verification. The public release notes state that the Windows +binaries are not Authenticode-signed and may display an Unknown Publisher +warning. + +The normal integrated frontend, backend, installer, Docker, Helm, macOS signing +and notarization, candidate validation, publication, activation, and convergence +controls remain unchanged. diff --git a/docs/release-control/v6/internal/status.json b/docs/release-control/v6/internal/status.json index b78d3644ca..35ac0a9560 100644 --- a/docs/release-control/v6/internal/status.json +++ b/docs/release-control/v6/internal/status.json @@ -10629,6 +10629,18 @@ "lane_ids": [ "L1" ] + }, + { + "id": "v6-3-1-unsigned-windows-owner-exception", + "summary": "On 2026-08-23 the release owner explicitly authorized unsigned Windows Unified Agent artifacts for stable v6.3.1 after exact-SHA rehearsal 32634435531 proved the SignPath production certificate remained CSR PENDING and the release-signing policy was invalid. This version-bound exception changes only the Authenticode requirement: the exact release SHA, immutable candidate manifest, SHA-256 checksums, detached signatures, published-digest verification, and public Unknown Publisher disclosure remain mandatory. Stable v6.3.2 and later restore mandatory Authenticode unless another explicit version-bound decision is recorded.", + "kind": "release-policy", + "decided_at": "2026-08-23", + "subsystem_ids": [ + "deployment-installability" + ], + "lane_ids": [ + "L1" + ] } ] } diff --git a/docs/release-control/v6/internal/subsystems/deployment-installability.md b/docs/release-control/v6/internal/subsystems/deployment-installability.md index b7da3010ad..c0a4a0def8 100644 --- a/docs/release-control/v6/internal/subsystems/deployment-installability.md +++ b/docs/release-control/v6/internal/subsystems/deployment-installability.md @@ -1680,9 +1680,15 @@ no-publication dry run and integrated candidate checks before the same SHA is dispatched through the single-build publication workflow. No governed mobile-facing path changed from `v6.3.0`, so the release decision is `no-mobile-impact` and no companion build or store rollout is required. -Windows Authenticode remains mandatory for `v6.3.1` unless the release owner -records a new explicit version-bound unsigned-Windows exception; the prior -`v6.3.0` decision cannot be reused for this patch. +Windows Authenticode was mandatory for `v6.3.1` unless the release owner +recorded a new explicit version-bound unsigned-Windows exception; the prior +`v6.3.0` decision could not be reused for this patch. On 2026-08-23 the release +owner recorded that separate `v6.3.1` exception after exact-SHA rehearsal +`32634435531` reached SignPath and proved the production certificate remained +`CSR PENDING`, leaving the `release-signing` policy invalid. This changes only +the Authenticode requirement: public Unknown Publisher disclosure and the +exact-SHA, checksum, detached-signature, immutable-manifest, and +published-digest controls remain mandatory. The preceding stable `v6.3.0` cut set the repo-root `VERSION`, repo-root `docker-compose.yml` image default, `scripts/install-docker.sh` diff --git a/docs/releases/RELEASE_NOTES_v6.3.1.md b/docs/releases/RELEASE_NOTES_v6.3.1.md index f208b91cce..cf2157d110 100644 --- a/docs/releases/RELEASE_NOTES_v6.3.1.md +++ b/docs/releases/RELEASE_NOTES_v6.3.1.md @@ -50,9 +50,11 @@ control, local subscription providers, and Docker monitoring overhead. - The exact pushed release SHA must pass the no-publication Release Dry Run and its integrated exact-SHA candidate checks before that same SHA is submitted to the single-build publication workflow. -- Authenticode signing is required through SignPath. The rehearsal must return - all three Windows Unified Agent binaries with valid signatures from the - configured production signer before the candidate can advance. +- The release owner approved a `v6.3.1`-only exception because SignPath's + production certificate remains CSR pending. Windows Unified Agent binaries + are not Authenticode-signed and may display an Unknown Publisher warning; + exact-SHA checksums, detached signatures, immutable-manifest verification, + and published-digest verification remain mandatory. - No mobile-facing path changed between `v6.3.0` and this release, so the mobile decision is `no-mobile-impact`; no companion build or store rollout is required. diff --git a/docs/releases/V6_CHANGELOG_v6.3.1.md b/docs/releases/V6_CHANGELOG_v6.3.1.md index 3389feabfa..163a4466ef 100644 --- a/docs/releases/V6_CHANGELOG_v6.3.1.md +++ b/docs/releases/V6_CHANGELOG_v6.3.1.md @@ -36,8 +36,9 @@ _This changelog describes stable `v6.3.1` compared with stable `v6.3.0`._ release workflow after an exact-SHA no-publication dry run - Emergency reason: active customer harm across notification recovery, Docker control, local subscription setup, and Synology Docker host load -- Windows signing decision: Authenticode signing is required through SignPath; - the exact candidate must prove the configured production signer before - publication +- Windows signing decision: version-bound `v6.3.1` owner exception because the + SignPath production certificate remains CSR pending; Windows binaries are + not Authenticode-signed and may display an Unknown Publisher warning, while + the exact-SHA integrity controls remain mandatory - Mobile decision: `no-mobile-impact`; no governed mobile-facing path changed from `v6.3.0`, so no companion build or public store rollout is required diff --git a/scripts/installtests/build_release_assets_test.go b/scripts/installtests/build_release_assets_test.go index fd7edcfd6a..0856b88aac 100644 --- a/scripts/installtests/build_release_assets_test.go +++ b/scripts/installtests/build_release_assets_test.go @@ -742,8 +742,10 @@ func TestCurrentStablePatchReleasePacketTracksInstallMetadata(t *testing.T) { "`v"+previous+"`", "Use the normal v6 install or update flow", "integrated exact-SHA candidate checks", - "Authenticode signing is required through SignPath", - "valid signatures from the configured production signer", + "`v6.3.1`-only exception", + "not Authenticode-signed", + "Unknown Publisher warning", + "exact-SHA checksums, detached signatures, immutable-manifest verification", "`no-mobile-impact`", "rollback target is `v"+previous+"`", ) @@ -751,8 +753,9 @@ func TestCurrentStablePatchReleasePacketTracksInstallMetadata(t *testing.T) { "Version: `v"+version+"`", "Rollback target: `v"+previous+"`", "Promotion path: emergency stable patch from `"+releaseBranch+"`", - "Windows signing decision: Authenticode signing is required through SignPath", - "configured production signer before publication", + "Windows signing decision: version-bound `v6.3.1` owner exception", + "not Authenticode-signed", + "Unknown Publisher warning", "Mobile decision: `no-mobile-impact`", ) assertFileContainsAll(t, repoFile("docs", "RELEASE_NOTES.md"), @@ -1342,12 +1345,12 @@ func TestReleaseCandidateRequiresPlatformNativeAgentSigning(t *testing.T) { ) assertFileContainsAll(t, repoFile(".github", "workflows", "release-dry-run.yml"), `Definitive Dry-Run Verdict`, - `require_windows_signing: ${{ !contains(inputs.version, '-') && !((inputs.version == '6.1.0' || inputs.version == '6.1.1' || inputs.version == '6.1.2' || inputs.version == '6.2.0' || inputs.version == '6.2.1' || inputs.version == '6.3.0') && inputs.unsigned_windows_exception) }}`, + `require_windows_signing: ${{ !contains(inputs.version, '-') && !((inputs.version == '6.1.0' || inputs.version == '6.1.1' || inputs.version == '6.1.2' || inputs.version == '6.2.0' || inputs.version == '6.2.1' || inputs.version == '6.3.0' || inputs.version == '6.3.1') && inputs.unsigned_windows_exception) }}`, `require_result "exact-SHA release candidate" "$CANDIDATE_RESULT" success`, `require_result "stable demo no-mutation verification" "$DEMO_RESULT" success`, ) assertFileContainsAll(t, repoFile("scripts", "release_control", "resolve_release_promotion.py"), - `version not in {"6.1.0", "6.1.1", "6.1.2", "6.2.0", "6.2.1", "6.3.0"}`, + `version not in {"6.1.0", "6.1.1", "6.1.2", "6.2.0", "6.2.1", "6.3.0", "6.3.1"}`, `unsigned_windows_reason is required`, `not Authenticode-signed`, `require_windows_signing = not is_prerelease and not unsigned_windows_exception`, diff --git a/scripts/installtests/install_docker_sh_test.go b/scripts/installtests/install_docker_sh_test.go index e25f3179a0..c6b9a8c7ed 100644 --- a/scripts/installtests/install_docker_sh_test.go +++ b/scripts/installtests/install_docker_sh_test.go @@ -302,10 +302,19 @@ func TestInstallDockerProofTracksStablePatchReleaseContract(t *testing.T) { "This patch release uses the stable hotfix path with `rollback_version=v"+previous+"`, `hotfix_exception=true`, a release-owner reason, and no fabricated same-version RC tag.", "active customer harm", "`no-mobile-impact`", - "Windows Authenticode remains mandatory for `v"+version+"`", - "prior `v"+previous+"` decision cannot be reused for this patch", "For the active stable `v"+version+"` cut, the repo-root compose default and `scripts/install-docker.sh` fallback must both pin `"+version+"`", ) + if version == "6.3.1" { + assertFileContainsAllNormalized(t, repoFile("docs", "release-control", "v6", "internal", "subsystems", "deployment-installability.md"), + "the prior `v"+previous+"` decision could not be reused for this patch", + "the release owner recorded that separate `v6.3.1` exception", + "public Unknown Publisher disclosure", + ) + } else { + assertFileContainsAllNormalized(t, repoFile("docs", "release-control", "v6", "internal", "subsystems", "deployment-installability.md"), + "Windows Authenticode remains mandatory for `v"+version+"`", + ) + } } func TestInstallDockerProofTracksStableMinorContract(t *testing.T) { diff --git a/scripts/release_control/release_promotion_policy_test.py b/scripts/release_control/release_promotion_policy_test.py index eb30971773..225e854d6c 100644 --- a/scripts/release_control/release_promotion_policy_test.py +++ b/scripts/release_control/release_promotion_policy_test.py @@ -1255,7 +1255,7 @@ class ReleasePromotionPolicyTest(unittest.TestCase): self.assertIn("if: ${{ inputs.version != '' }}", workflow) self.assertIn("require_macos_signing: true", workflow) self.assertIn( - "require_windows_signing: ${{ !contains(inputs.version, '-') && !((inputs.version == '6.1.0' || inputs.version == '6.1.1' || inputs.version == '6.1.2' || inputs.version == '6.2.0' || inputs.version == '6.2.1' || inputs.version == '6.3.0') && inputs.unsigned_windows_exception) }}", + "require_windows_signing: ${{ !contains(inputs.version, '-') && !((inputs.version == '6.1.0' || inputs.version == '6.1.1' || inputs.version == '6.1.2' || inputs.version == '6.2.0' || inputs.version == '6.2.1' || inputs.version == '6.3.0' || inputs.version == '6.3.1') && inputs.unsigned_windows_exception) }}", workflow, ) self.assertIn("unsigned_windows_exception:", workflow) @@ -1455,7 +1455,7 @@ class ReleasePromotionPolicyTest(unittest.TestCase): self.assertIn("require_windows_signing: ${{ needs.prepare.outputs.require_windows_signing == 'true' }}", content) self.assertIn("unsigned_windows_exception:", content) self.assertIn("unsigned_windows_reason:", content) - self.assertIn('version not in {"6.1.0", "6.1.1", "6.1.2", "6.2.0", "6.2.1", "6.3.0"}', resolver) + self.assertIn('version not in {"6.1.0", "6.1.1", "6.1.2", "6.2.0", "6.2.1", "6.3.0", "6.3.1"}', resolver) self.assertIn("not Authenticode-signed", resolver) self.assertIn("windows_signing_backend: signpath", content) self.assertIn('if [[ "$REQUIRE_WINDOWS_SIGNING" == "true" ]]', candidate_workflow) @@ -1739,6 +1739,18 @@ class ReleasePromotionPolicyTest(unittest.TestCase): self.assertIn("not Authenticode-signed", signing_record) self.assertIn("Unknown Publisher warning", signing_record) + patch_signing_record = normalize_ws( + read( + "docs/release-control/v6/internal/records/" + "v6.3.1-unsigned-windows-owner-approval-2026-08-23.md" + ) + ) + self.assertIn("v6.3.1 unsigned-Windows exception", policy) + self.assertIn("Stable `v6.3.2` and later restore mandatory Authenticode", policy) + self.assertIn("SignPath production certificate remains `CSR PENDING`", patch_signing_record) + self.assertIn("not Authenticode-signed", patch_signing_record) + self.assertIn("Unknown Publisher warning", patch_signing_record) + def test_release_artifact_workflows_refuse_stable_without_matching_rc(self) -> None: publish = read(".github/workflows/publish-docker.yml") promote = read(".github/workflows/promote-floating-tags.yml") diff --git a/scripts/release_control/resolve_release_promotion.py b/scripts/release_control/resolve_release_promotion.py index c945747c33..120661aef4 100644 --- a/scripts/release_control/resolve_release_promotion.py +++ b/scripts/release_control/resolve_release_promotion.py @@ -245,10 +245,10 @@ def resolve_metadata( promotion_mode = "prerelease" if is_prerelease else "stable-rc-promotion" if unsigned_windows_exception: - if version not in {"6.1.0", "6.1.1", "6.1.2", "6.2.0", "6.2.1", "6.3.0"}: + if version not in {"6.1.0", "6.1.1", "6.1.2", "6.2.0", "6.2.1", "6.3.0", "6.3.1"}: raise ValueError( "unsigned_windows_exception is approved only for stable v6.1.0, v6.1.1, " - "v6.1.2, v6.2.0, v6.2.1, or v6.3.0. Later stable releases require a new explicit, " + "v6.1.2, v6.2.0, v6.2.1, v6.3.0, or v6.3.1. Later stable releases require a new explicit, " "version-bound owner decision." ) if not unsigned_windows_reason: diff --git a/scripts/release_control/resolve_release_promotion_test.py b/scripts/release_control/resolve_release_promotion_test.py index 71050aa50c..f9981dc671 100644 --- a/scripts/release_control/resolve_release_promotion_test.py +++ b/scripts/release_control/resolve_release_promotion_test.py @@ -318,15 +318,45 @@ class ResolveReleasePromotionTest(unittest.TestCase): self.assertEqual(metadata["require_windows_signing"], "false") self.assertEqual(metadata["unsigned_windows_exception"], "true") + def test_v631_owner_exception_allows_disclosed_emergency_patch(self) -> None: + metadata = resolver.resolve_metadata( + version="6.3.1", + promoted_from_tag_input="", + rollback_version_input="v6.3.0", + ga_date_input="", + v5_eos_date_input="", + hotfix_exception=True, + hotfix_reason_input="Active customer-impact fixes.", + release_notes_input=( + "Windows Unified Agent binaries are not Authenticode-signed for v6.3.1." + ), + unsigned_windows_exception=True, + unsigned_windows_reason_input=( + "The SignPath production certificate remains CSR pending; the release owner " + "accepts unsigned Windows binaries for v6.3.1." + ), + list_stable_tags_fn=lambda: ["v6.3.0", "v6.2.1"], + list_same_version_rc_tags_fn=lambda version: [], + changed_paths_fn=lambda tag: ["scripts/install.sh"], + tag_exists_fn=lambda tag: tag == "v6.3.0", + tag_commit_fn=lambda tag: "v630-commit", + head_descends_from_fn=lambda commit: commit == "v630-commit", + ) + + self.assertEqual(metadata["promotion_mode"], "emergency-stable-patch") + self.assertEqual(metadata["rollback_tag"], "v6.3.0") + self.assertEqual(metadata["require_windows_signing"], "false") + self.assertEqual(metadata["unsigned_windows_exception"], "true") + def test_unsigned_windows_exception_is_rejected_for_other_stable_versions(self) -> None: with self.assertRaisesRegex( ValueError, - "approved only for stable v6.1.0, v6.1.1, v6.1.2, v6.2.0, v6.2.1, or v6.3.0", + "approved only for stable v6.1.0, v6.1.1, v6.1.2, v6.2.0, v6.2.1, v6.3.0, or v6.3.1", ): resolver.resolve_metadata( - version="6.3.1", + version="6.3.2", promoted_from_tag_input="", - rollback_version_input="v6.3.0", + rollback_version_input="v6.3.1", ga_date_input="", v5_eos_date_input="", hotfix_exception=True,