From 9cd594a0bf248783f20cd6f7745340105b01ac81 Mon Sep 17 00:00:00 2001 From: Alberto Pose Date: Wed, 13 May 2026 16:57:04 +0100 Subject: [PATCH] Fail crossbuild loudly when Windows signing silently skips The signing block in `scripts/crossbuild.mk` wraps `az login` / `jsign` in a `set -e; if [[ ... ]]; then ...; fi` recipe line. POSIX `set -e` is documented to be suppressed inside `if` condition lists, so when `[[` fails (e.g. the recipe falls back to /bin/sh on a host where /bin/sh is dash), the condition silently returns false, the then-block is skipped, and the recipe exits 0 with an unsigned binary while CI stays green. Add `scripts/verify_signed.py`, a small presence check that parses the PE Optional Header's Certificate Table directory entry and exits non- zero if it is empty. Wire it into the `bin/%/$(PROVIDER).exe` recipe as its own recipe line, guarded so it only runs when signing was expected: @[ "${GOOS}" != "windows" ] || [ "${SKIP_SIGNING}" = "true" ] || \ python3 scripts/verify_signed.py "$@" Because the verify is its own recipe line (no `if`-condition wrapping), make checks its exit code directly and fails the recipe on a missing signature. The set-e-inside-if antipattern can no longer hide a silently-skipped signing step. Part of pulumi/home#4655, pulumi/home#4656, pulumi/home#4657. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../pkg/templates/base/scripts/crossbuild.mk | 6 +++ .../templates/base/scripts/verify_signed.py | 51 +++++++++++++++++++ .../test-providers/aws/scripts/crossbuild.mk | 6 +++ .../aws/scripts/verify_signed.py | 51 +++++++++++++++++++ .../cloudflare/scripts/crossbuild.mk | 6 +++ .../cloudflare/scripts/verify_signed.py | 51 +++++++++++++++++++ .../docker/scripts/crossbuild.mk | 6 +++ .../docker/scripts/verify_signed.py | 51 +++++++++++++++++++ .../test-providers/eks/scripts/crossbuild.mk | 6 +++ .../eks/scripts/verify_signed.py | 51 +++++++++++++++++++ .../pulumiservice/scripts/crossbuild.mk | 6 +++ .../pulumiservice/scripts/verify_signed.py | 51 +++++++++++++++++++ .../terraform-module/scripts/crossbuild.mk | 6 +++ .../terraform-module/scripts/verify_signed.py | 51 +++++++++++++++++++ .../test-providers/xyz/scripts/crossbuild.mk | 6 +++ .../xyz/scripts/verify_signed.py | 51 +++++++++++++++++++ 16 files changed, 456 insertions(+) create mode 100644 provider-ci/internal/pkg/templates/base/scripts/verify_signed.py create mode 100644 provider-ci/test-providers/aws/scripts/verify_signed.py create mode 100644 provider-ci/test-providers/cloudflare/scripts/verify_signed.py create mode 100644 provider-ci/test-providers/docker/scripts/verify_signed.py create mode 100644 provider-ci/test-providers/eks/scripts/verify_signed.py create mode 100644 provider-ci/test-providers/pulumiservice/scripts/verify_signed.py create mode 100644 provider-ci/test-providers/terraform-module/scripts/verify_signed.py create mode 100644 provider-ci/test-providers/xyz/scripts/verify_signed.py diff --git a/provider-ci/internal/pkg/templates/base/scripts/crossbuild.mk b/provider-ci/internal/pkg/templates/base/scripts/crossbuild.mk index 83d5cb3ab8..ecc2d794f6 100644 --- a/provider-ci/internal/pkg/templates/base/scripts/crossbuild.mk +++ b/provider-ci/internal/pkg/templates/base/scripts/crossbuild.mk @@ -59,6 +59,12 @@ bin/%/$(PROVIDER) bin/%/$(PROVIDER).exe: bin/jsign-7.4.jar fi; \ fi + @# Verify the windows binary was actually signed when signing was expected. + @# This runs in its own shell as a separate recipe line, so make checks its + @# exit code directly (no set-e-inside-if antipattern). Catches the silent + @# failure where the signing block above exits 0 without producing a signature. + @[ "${GOOS}" != "windows" ] || [ "${SKIP_SIGNING}" = "true" ] || python3 scripts/verify_signed.py "$@" + bin/jsign-7.4.jar: wget https://github.com/ebourg/jsign/releases/download/7.4/jsign-7.4.jar --output-document=bin/jsign-7.4.jar diff --git a/provider-ci/internal/pkg/templates/base/scripts/verify_signed.py b/provider-ci/internal/pkg/templates/base/scripts/verify_signed.py new file mode 100644 index 0000000000..06816dfabe --- /dev/null +++ b/provider-ci/internal/pkg/templates/base/scripts/verify_signed.py @@ -0,0 +1,51 @@ +"""Verify a Windows PE binary has an Authenticode signature attached. + +Presence check only: parses the PE Optional Header's Certificate Table +directory entry and exits 0 if it is non-empty, non-zero otherwise. Does +not validate the certificate chain or the signature itself; full chain +validation is the verify-release workflow's job. + +Purpose: catch the silent-failure pattern in scripts/crossbuild.mk where +the signing block exits 0 without producing a signature (e.g. a future +regression that returns the recipe to /bin/sh and silently skips the +bash conditional). + +Usage: python3 scripts/verify_signed.py +""" + +import struct +import sys + + +def main(argv: list[str]) -> int: + if len(argv) != 2: + print( + f"usage: {argv[0]} ", + file=sys.stderr, + ) + return 2 + path = argv[1] + with open(path, "rb") as f: + data = f.read() + + # PE header offset is stored at 0x3C. + pe = struct.unpack_from(" +""" + +import struct +import sys + + +def main(argv: list[str]) -> int: + if len(argv) != 2: + print( + f"usage: {argv[0]} ", + file=sys.stderr, + ) + return 2 + path = argv[1] + with open(path, "rb") as f: + data = f.read() + + # PE header offset is stored at 0x3C. + pe = struct.unpack_from(" +""" + +import struct +import sys + + +def main(argv: list[str]) -> int: + if len(argv) != 2: + print( + f"usage: {argv[0]} ", + file=sys.stderr, + ) + return 2 + path = argv[1] + with open(path, "rb") as f: + data = f.read() + + # PE header offset is stored at 0x3C. + pe = struct.unpack_from(" +""" + +import struct +import sys + + +def main(argv: list[str]) -> int: + if len(argv) != 2: + print( + f"usage: {argv[0]} ", + file=sys.stderr, + ) + return 2 + path = argv[1] + with open(path, "rb") as f: + data = f.read() + + # PE header offset is stored at 0x3C. + pe = struct.unpack_from(" +""" + +import struct +import sys + + +def main(argv: list[str]) -> int: + if len(argv) != 2: + print( + f"usage: {argv[0]} ", + file=sys.stderr, + ) + return 2 + path = argv[1] + with open(path, "rb") as f: + data = f.read() + + # PE header offset is stored at 0x3C. + pe = struct.unpack_from(" +""" + +import struct +import sys + + +def main(argv: list[str]) -> int: + if len(argv) != 2: + print( + f"usage: {argv[0]} ", + file=sys.stderr, + ) + return 2 + path = argv[1] + with open(path, "rb") as f: + data = f.read() + + # PE header offset is stored at 0x3C. + pe = struct.unpack_from(" +""" + +import struct +import sys + + +def main(argv: list[str]) -> int: + if len(argv) != 2: + print( + f"usage: {argv[0]} ", + file=sys.stderr, + ) + return 2 + path = argv[1] + with open(path, "rb") as f: + data = f.read() + + # PE header offset is stored at 0x3C. + pe = struct.unpack_from(" +""" + +import struct +import sys + + +def main(argv: list[str]) -> int: + if len(argv) != 2: + print( + f"usage: {argv[0]} ", + file=sys.stderr, + ) + return 2 + path = argv[1] + with open(path, "rb") as f: + data = f.read() + + # PE header offset is stored at 0x3C. + pe = struct.unpack_from("