Record v6.2.0 Windows signing exception

This commit is contained in:
rcourtman
2026-08-09 11:03:15 +01:00
parent f962fe83b1
commit e2a2e7d4d2
16 changed files with 121 additions and 49 deletions
+2 -2
View File
@@ -38,12 +38,12 @@ on:
required: false
type: string
unsigned_windows_exception:
description: 'v6.1.0, v6.1.1, or v6.1.2 only: publish Windows agents without Authenticode under a recorded owner exception'
description: 'v6.1.0, v6.1.1, v6.1.2, or v6.2.0 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, or v6.1.2 only: owner reason for the unsigned Windows exception'
description: 'v6.1.0, v6.1.1, v6.1.2, or v6.2.0 only: owner reason for the unsigned Windows exception'
required: false
type: string
historical_asset_backfill_only:
+3 -3
View File
@@ -43,12 +43,12 @@ on:
required: false
type: string
unsigned_windows_exception:
description: 'v6.1.0, v6.1.1, or v6.1.2 only: rehearse Windows agents without Authenticode under a recorded owner exception'
description: 'v6.1.0, v6.1.1, v6.1.2, or v6.2.0 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, or v6.1.2 only: owner reason for the unsigned Windows exception'
description: 'v6.1.0, v6.1.1, v6.1.2, or v6.2.0 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.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.unsigned_windows_exception) }}
windows_signing_backend: signpath
dry-run:
@@ -1082,8 +1082,8 @@ Companion drill:
release packet, independent post-release jobs are serialized, or the
definitive verdict can pass without all applicable downstream results. Also
block if an unsigned stable Windows exception can apply to any version other
than the recorded `v6.1.0`, `v6.1.1`, and `v6.1.2` decisions or can omit its owner
reason and release-note disclosure.
than the recorded `v6.1.0`, `v6.1.1`, `v6.1.2`, and `v6.2.0` decisions or can
omit its owner reason and release-note disclosure.
## Gate: `operational-trust-canonical-lifecycle`
@@ -35,7 +35,11 @@ Use this as the final gate before cutting a Pulse v6 pre-release.
- The 2026-08-09 v6.2.0 stable-cutoff decision separately authorizes promotion
from `v6.2.0-rc.11` without completing the normal 72-hour soak. It is a
version-bound owner-risk acceptance, not soak evidence; the exact stable SHA
still requires the no-publication dry run and mandatory SignPath proof.
still requires the no-publication dry run. After exact-SHA dry run
`31306697834` failed closed because the SignPath release certificate CSR
remained pending, the release owner separately approved a v6.2.0-only
unsigned-Windows exception with mandatory public disclosure and exact-SHA,
checksum, detached-signature, manifest, and published-digest verification.
- `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.
@@ -59,7 +63,8 @@ Use this as the final gate before cutting a Pulse v6 pre-release.
- [x] For stable promotion, record the 2026-07-02 release-owner decision accepting the current-branch validation risk for the post-RC7 changes.
- [x] For stable v6.2.0 promotion, record the 2026-08-09 release-owner decision
accepting the shortened RC11 soak and bind it to the exact content cutoff,
rollback command, dry-run requirement, and mandatory SignPath path.
rollback command, dry-run requirement, and the separate v6.2.0-only unsigned
Windows decision recorded after the SignPath CSR-pending failure.
- [x] For GA/stable promotion, confirm `V5_MAINTENANCE_SUPPORT_POLICY.md` is still the intended policy and replace any placeholder GA notice dates with the exact v6 GA date and exact v5 end-of-support date that will ship with the announcement.
- [x] For GA/stable promotion, confirm the pushed governed release-branch copy of `.github/workflows/release-dry-run.yml` already accepts the governed stable rehearsal metadata envelope (`promoted_from_tag`, `rollback_version`, `ga_date`, `v5_eos_date`) through `workflow_dispatch`, because GitHub executes the selected remote ref and does not see local-only governance state.
- [x] For GA/stable promotion, confirm the local rehearsal branch exactly matches `origin` before dispatching `Release Dry Run`, so the run exercises the intended governed branch state instead of stale remote control-plane metadata.
@@ -277,8 +277,18 @@ TLS-unverified receipts leave the claim at `implemented` or
release-note, qualification, test-guardrail, and release-control metadata.
The exact pushed SHA must pass the no-publication `Release Dry Run` before
the same SHA is dispatched through the single-build publication workflow.
- Stable v6.2.0 restores mandatory Windows Authenticode signing through
SignPath. No unsigned-Windows exception is authorized by this soak waiver.
- Exact-SHA dry run `31306697834` failed closed before candidate assembly or
public mutation because the SignPath `release-signing` policy was invalid:
its `Release certificate 2026` CSR remained pending. After that failure,
the release owner separately approved a `v6.2.0`-only unsigned-Windows
exception. This signing exception is independent of the soak waiver and is
not a standing decision for later releases.
- The unsigned Windows artifacts remain bound by the exact-SHA candidate
manifest, checksums, detached `.sig`/`.sshsig` signatures, and published
digests. Public notes must disclose that they are not Authenticode-signed
and may display an Unknown Publisher warning. Stable `v6.2.1` and later
restore mandatory Windows Authenticode unless another explicit,
version-bound owner decision is recorded.
## Single-Build Release Path
@@ -27,10 +27,20 @@ above.
## Windows Signing Boundary
Stable v6.2.0 restores mandatory Authenticode signing through SignPath. No
unsigned-Windows exception is granted by this approval. The release must fail
closed before publication if SignPath configuration, returned signer identity,
or signed-artifact verification is unavailable.
Exact-SHA dry run `31306697834` failed closed before candidate assembly or any
public mutation because SignPath reported the `release-signing` policy invalid;
its `Release certificate 2026` CSR remained pending. After that failure, the
release owner explicitly approved a `v6.2.0`-only unsigned-Windows exception so
the stable release can proceed while the external certificate request remains
unavailable.
This is a separate, version-bound decision and is not implied by the soak
waiver. Windows artifacts must remain bound to the exact source SHA and
candidate manifest, checksums, detached `.sig`/`.sshsig` signatures, and
published-digest verification. Public release notes must disclose that the
Windows binaries are not Authenticode-signed and may display an Unknown
Publisher warning. Stable `v6.2.1` and later restore mandatory Authenticode
signing unless another explicit version-bound owner decision is recorded.
## Cutoff Reopen Rule
@@ -476,13 +476,13 @@ upgrade, update, release, or artifact-selection behavior.
apply the same channel-specific native-signing policy as a publish run.
macOS notarization remains mandatory for both prerelease and stable
candidates. Windows Authenticode remains mandatory for stable candidates
except for the explicitly version-bound `v6.1.0`, `v6.1.1`, and `v6.1.2`
owner exceptions; prerelease candidates and those three stable exceptions may
except for the explicitly version-bound `v6.1.0`, `v6.1.1`, `v6.1.2`, and
`v6.2.0` owner exceptions; prerelease candidates and those four stable exceptions may
retain checksum and detached-signature verification without Authenticode
while the release packet explicitly discloses the unknown-publisher warning.
Prerelease promotion remains blocked on the normal stable signing
requirement, and stable `v6.1.3` and later restore it automatically unless
policy records a new version-bound owner decision. A cheap
requirement. Stable releases after each recorded exception restore it
automatically unless policy records a new version-bound owner decision. A cheap
signing-configuration job
must report every missing secret for the platforms required by that
candidate before either platform runner is allocated. Stable Windows signing must use SignPath's GitHub
@@ -1395,8 +1395,12 @@ remain distributed to the existing beta cohort through TestFlight and Play
open testing. The v6.2.0 changes preserve the checked-in mobile API, Relay,
pairing, approval, push, authentication, and onboarding contracts; no
additional companion upload or public store rollout is part of the stable
server cut. Stable Windows artifacts must complete the SignPath Authenticode
path; the soak waiver grants no unsigned-Windows exception.
server cut. Exact-SHA dry run `31306697834` failed closed before candidate
assembly because the SignPath `Release certificate 2026` CSR remained pending.
The release owner then separately approved a v6.2.0-only unsigned-Windows
exception. The Windows packet must disclose the Unknown Publisher warning and
retain exact-SHA, checksum, detached-signature, manifest, and published-digest
verification; this decision is not inherited by later releases.
The first RC11 publication attempt, run `31274524321`, passed every immutable
release gate and briefly crossed the draft boundary, then failed before the
irreversible activation marker because the checkout-free activation job relied
@@ -1462,10 +1466,13 @@ with `rollback_version=v6.1.2` and pinned the same four install surfaces to
pins; its packet stays in `docs/releases/` as the historical candidate record
for the `v6.2.0` line.
Authenticode signing through SignPath is the canonical Windows signing backend
for the `v6.2.0` line. The owner-approved unsigned-Windows exception is bounded
to `v6.1.0`, `v6.1.1`, and `v6.1.2` and does not extend to any `v6.2.0`
release, so stable `v6.2.0` promotion must fail closed when the SignPath
configuration or the returned signer proof is unavailable.
for the `v6.2.0` line. After exact-SHA dry run `31306697834` failed closed on
the pending SignPath release-certificate CSR, the release owner approved a
`v6.2.0`-only unsigned-Windows exception. It preserves the exact-SHA candidate,
checksum, detached-signature, manifest, published-digest, owner-reason, and
public Unknown Publisher disclosure controls. Stable `v6.2.1` and later
restore mandatory Authenticode unless policy records another explicit,
version-bound decision.
The active stable `v6.1.2` cut sets the repo-root `VERSION`, repo-root
`docker-compose.yml` image default, `scripts/install-docker.sh` fallback, and
@@ -3143,9 +3150,9 @@ 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 except for the recorded, version-bound
`v6.1.0`, `v6.1.1`, and `v6.1.2` Windows exceptions; stable `v6.1.3` and later restore
both requirements unless policy records a new explicit version-bound owner
decision. `scripts/build-release.sh` must replace
`v6.1.0`, `v6.1.1`, `v6.1.2`, and `v6.2.0` Windows exceptions; subsequent
stable versions restore both requirements unless policy records a new explicit
version-bound owner decision. `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
+5 -2
View File
@@ -71,8 +71,11 @@ the final stable cutoff.
v6.2.0; that version-bound decision is risk acceptance, not soak evidence.
- The exact pushed stable SHA must pass the no-publication Release Dry Run
before the same SHA enters the single-build publication workflow.
- Stable Windows Unified Agent binaries are Authenticode-signed through
SignPath. No unsigned-Windows exception applies to v6.2.0.
- Windows Unified Agent binaries in v6.2.0 are not Authenticode-signed and may
show an Unknown Publisher warning. Verify their checksums and detached
`.sig`/`.sshsig` signatures. This is a v6.2.0-only owner exception while the
SignPath release certificate CSR remains pending; later releases restore the
signing requirement unless separately approved.
## Upgrade Notes
+5 -3
View File
@@ -55,9 +55,11 @@ _This changelog describes stable `v6.2.0` compared with stable `v6.1.2`._
v6.2.0-only soak waiver and the single-build release workflow
- Soak decision: the release owner waived the remainder of the normal 72-hour
RC11 soak for v6.2.0; this is version-bound risk acceptance, not soak evidence
- Windows signing decision: stable Windows Unified Agent artifacts require
Authenticode signing and signer verification through SignPath; no unsigned
exception applies
- Windows signing decision: the v6.2.0-only owner exception permits Windows
Unified Agent artifacts that are not Authenticode-signed while the SignPath
release certificate CSR remains pending; users receive an Unknown Publisher
disclosure and checksum, detached-signature, exact-SHA, manifest, and
published-digest verification remain mandatory
- Mobile decision: `existing-mobile-build-compatible`; Pulse Mobile 1.0.0 iOS
build 12 and Android versionCode 9 remain the compatible beta candidates,
both using runtime version 2, with no companion upload or public store rollout
@@ -586,7 +586,8 @@ func TestCurrentStableMinorReleasePacketTracksInstallMetadata(t *testing.T) {
"Operational Trust",
"Actions provides a dedicated inbox",
"existing Pulse Mobile candidate",
"Authenticode-signed through SignPath",
"not Authenticode-signed",
"Unknown Publisher warning",
"The rollback target is `v"+previous+"`",
)
assertFileContainsAllNormalized(t, changelogPath,
@@ -1083,12 +1084,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.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.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"}`,
`version not in {"6.1.0", "6.1.1", "6.1.2", "6.2.0"}`,
`unsigned_windows_reason is required`,
`not Authenticode-signed`,
`require_windows_signing = not is_prerelease and not unsigned_windows_exception`,
@@ -327,7 +327,8 @@ func TestInstallDockerProofTracksStableMinorContract(t *testing.T) {
"The active stable `v"+version+"` cut sets the repo-root `VERSION`, repo-root `docker-compose.yml` image default, `scripts/install-docker.sh` fallback, and Helm chart release metadata to the same `"+version+"` release version.",
"`rollback_version=v"+previous+"`",
"`hotfix_exception=true` transports that approved waiver through the shared promotion resolver; it does not reclassify v"+version+" as a patch hotfix.",
"Stable Windows artifacts must complete the SignPath Authenticode path; the soak waiver grants no unsigned-Windows exception.",
"The release owner then separately approved a v"+version+"-only unsigned-Windows exception.",
"The Windows packet must disclose the Unknown Publisher warning and retain exact-SHA, checksum, detached-signature, manifest, and published-digest verification",
"For the active stable `v"+version+"` cut, the repo-root compose default and `scripts/install-docker.sh` fallback must both pin `"+version+"`",
)
}
@@ -1238,7 +1238,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.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.unsigned_windows_exception) }}",
workflow,
)
self.assertIn("unsigned_windows_exception:", workflow)
@@ -1436,7 +1436,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"}', resolver)
self.assertIn('version not in {"6.1.0", "6.1.1", "6.1.2", "6.2.0"}', resolver)
self.assertIn("not Authenticode-signed", resolver)
self.assertIn("windows_signing_backend: signpath", content)
self.assertIn('if [[ "$REQUIRE_WINDOWS_SIGNING" == "true" ]]', candidate_workflow)
@@ -1654,14 +1654,17 @@ class ReleasePromotionPolicyTest(unittest.TestCase):
self.assertIn("v6.2.0 release-cutoff exception", policy)
self.assertIn("not soak evidence and not a standing exception", policy)
self.assertIn("mandatory Windows Authenticode signing through SignPath", policy)
self.assertIn("`v6.2.0`-only unsigned-Windows exception", policy)
self.assertIn("not a standing decision for later releases", policy)
self.assertIn("Promoted prerelease: `v6.2.0-rc.11`", owner_record)
self.assertIn("Rollback target: `v6.1.2`", owner_record)
self.assertIn(
"Exact rollback reinstall command: `./scripts/install.sh --version v6.1.2`",
owner_record,
)
self.assertIn("No unsigned-Windows exception is granted", owner_record)
self.assertIn("explicitly approved a `v6.2.0`-only unsigned-Windows exception", owner_record)
self.assertIn("not Authenticode-signed", owner_record)
self.assertIn("Unknown Publisher warning", owner_record)
def test_release_artifact_workflows_refuse_stable_without_matching_rc(self) -> None:
publish = read(".github/workflows/publish-docker.yml")
@@ -245,10 +245,11 @@ 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"}:
if version not in {"6.1.0", "6.1.1", "6.1.2", "6.2.0"}:
raise ValueError(
"unsigned_windows_exception is approved only for stable v6.1.0, v6.1.1, or v6.1.2. "
"Stable v6.1.3 and later must restore Windows Authenticode signing."
"unsigned_windows_exception is approved only for stable v6.1.0, v6.1.1, "
"v6.1.2, or v6.2.0. Later stable releases require a new explicit, "
"version-bound owner decision."
)
if not unsigned_windows_reason:
raise ValueError(
@@ -230,10 +230,39 @@ class ResolveReleasePromotionTest(unittest.TestCase):
self.assertEqual(metadata["require_windows_signing"], "false")
self.assertEqual(metadata["unsigned_windows_exception"], "true")
def test_v620_owner_exception_allows_disclosed_stable_promotion(self) -> None:
metadata = resolver.resolve_metadata(
version="6.2.0",
promoted_from_tag_input="v6.2.0-rc.11",
rollback_version_input="v6.1.2",
ga_date_input="",
v5_eos_date_input="",
hotfix_exception=True,
hotfix_reason_input="Owner waived the remaining prerelease soak.",
release_notes_input=(
"Windows Unified Agent binaries are not Authenticode-signed for v6.2.0."
),
unsigned_windows_exception=True,
unsigned_windows_reason_input=(
"The release certificate CSR remains pending; the release owner accepts "
"unsigned Windows binaries for v6.2.0."
),
tag_exists_fn=lambda tag: tag in {"v6.2.0-rc.11", "v6.1.2"},
tag_commit_fn=lambda tag: "rc11-commit",
head_descends_from_fn=lambda commit: commit == "rc11-commit",
tag_created_unix_fn=lambda tag: 100,
now_unix_fn=lambda: 100 + (13 * 3600),
)
self.assertEqual(metadata["promotion_mode"], "stable-rc-promotion")
self.assertEqual(metadata["rollback_tag"], "v6.1.2")
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, or v6.1.2",
"approved only for stable v6.1.0, v6.1.1, v6.1.2, or v6.2.0",
):
resolver.resolve_metadata(
version="6.1.3",
+2 -2
View File
@@ -171,9 +171,9 @@ if [ "$IS_PRERELEASE" != "true" ]; then
fi
fi
if [ "$VERSION" = "6.1.0" ]; then
if [[ "$VERSION" =~ ^6\.(1\.[012]|2\.0)$ ]]; then
echo ""
read -r -p "Use the recorded v6.1.0 unsigned Windows exception? [y/N] " UNSIGNED_WINDOWS_REPLY
read -r -p "Use the recorded v${VERSION} unsigned Windows exception? [y/N] " UNSIGNED_WINDOWS_REPLY
if [[ "$UNSIGNED_WINDOWS_REPLY" =~ ^[Yy]$ ]]; then
UNSIGNED_WINDOWS_EXCEPTION="true"
read -r -p "Unsigned Windows exception reason: " UNSIGNED_WINDOWS_REASON
+2 -2
View File
@@ -261,9 +261,9 @@ if [ "$IS_PRERELEASE" != "true" ]; then
fi
fi
if [ "$VERSION" = "6.1.0" ]; then
if [[ "$VERSION" =~ ^6\.(1\.[012]|2\.0)$ ]]; then
echo ""
read -r -p "Use the recorded v6.1.0 unsigned Windows exception? [y/N] " UNSIGNED_WINDOWS_REPLY
read -r -p "Use the recorded v${VERSION} unsigned Windows exception? [y/N] " UNSIGNED_WINDOWS_REPLY
if [[ "$UNSIGNED_WINDOWS_REPLY" =~ ^[Yy]$ ]]; then
UNSIGNED_WINDOWS_EXCEPTION="true"
read -r -p "Unsigned Windows exception reason: " UNSIGNED_WINDOWS_REASON