Skip to content

Commit 9cd594a

Browse files
poseclaude
andcommitted
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) <noreply@anthropic.com>
1 parent 0074f3a commit 9cd594a

16 files changed

Lines changed: 456 additions & 0 deletions

File tree

provider-ci/internal/pkg/templates/base/scripts/crossbuild.mk

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,12 @@ bin/%/$(PROVIDER) bin/%/$(PROVIDER).exe: bin/jsign-7.4.jar
5959
fi; \
6060
fi
6161

62+
@# Verify the windows binary was actually signed when signing was expected.
63+
@# This runs in its own shell as a separate recipe line, so make checks its
64+
@# exit code directly (no set-e-inside-if antipattern). Catches the silent
65+
@# failure where the signing block above exits 0 without producing a signature.
66+
@[ "${GOOS}" != "windows" ] || [ "${SKIP_SIGNING}" = "true" ] || python3 scripts/verify_signed.py "$@"
67+
6268
bin/jsign-7.4.jar:
6369
wget https://github.com/ebourg/jsign/releases/download/7.4/jsign-7.4.jar --output-document=bin/jsign-7.4.jar
6470

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
"""Verify a Windows PE binary has an Authenticode signature attached.
2+
3+
Presence check only: parses the PE Optional Header's Certificate Table
4+
directory entry and exits 0 if it is non-empty, non-zero otherwise. Does
5+
not validate the certificate chain or the signature itself; full chain
6+
validation is the verify-release workflow's job.
7+
8+
Purpose: catch the silent-failure pattern in scripts/crossbuild.mk where
9+
the signing block exits 0 without producing a signature (e.g. a future
10+
regression that returns the recipe to /bin/sh and silently skips the
11+
bash conditional).
12+
13+
Usage: python3 scripts/verify_signed.py <path-to-exe>
14+
"""
15+
16+
import struct
17+
import sys
18+
19+
20+
def main(argv: list[str]) -> int:
21+
if len(argv) != 2:
22+
print(
23+
f"usage: {argv[0]} <path-to-exe>",
24+
file=sys.stderr,
25+
)
26+
return 2
27+
path = argv[1]
28+
with open(path, "rb") as f:
29+
data = f.read()
30+
31+
# PE header offset is stored at 0x3C.
32+
pe = struct.unpack_from("<I", data, 0x3C)[0]
33+
# Optional header Magic: 0x10B = PE32, 0x20B = PE32+.
34+
magic = struct.unpack_from("<H", data, pe + 24)[0]
35+
# DataDirectory starts at +96 (PE32) or +112 (PE32+) of optional header.
36+
# Certificate Table is index 4; each entry is 8 bytes (VirtualAddress, Size).
37+
dd_start = pe + 24 + (96 if magic == 0x10B else 112)
38+
cert_size = struct.unpack_from("<I", data, dd_start + 4 * 8 + 4)[0]
39+
40+
if cert_size == 0:
41+
print(
42+
f"ERROR: {path} has no Authenticode signature attached. "
43+
"The signing step in crossbuild.mk silently failed.",
44+
file=sys.stderr,
45+
)
46+
return 1
47+
return 0
48+
49+
50+
if __name__ == "__main__":
51+
sys.exit(main(sys.argv))

provider-ci/test-providers/aws/scripts/crossbuild.mk

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,12 @@ bin/%/$(PROVIDER) bin/%/$(PROVIDER).exe: bin/jsign-7.4.jar
5959
fi; \
6060
fi
6161

62+
@# Verify the windows binary was actually signed when signing was expected.
63+
@# This runs in its own shell as a separate recipe line, so make checks its
64+
@# exit code directly (no set-e-inside-if antipattern). Catches the silent
65+
@# failure where the signing block above exits 0 without producing a signature.
66+
@[ "${GOOS}" != "windows" ] || [ "${SKIP_SIGNING}" = "true" ] || python3 scripts/verify_signed.py "$@"
67+
6268
bin/jsign-7.4.jar:
6369
wget https://github.com/ebourg/jsign/releases/download/7.4/jsign-7.4.jar --output-document=bin/jsign-7.4.jar
6470

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
"""Verify a Windows PE binary has an Authenticode signature attached.
2+
3+
Presence check only: parses the PE Optional Header's Certificate Table
4+
directory entry and exits 0 if it is non-empty, non-zero otherwise. Does
5+
not validate the certificate chain or the signature itself; full chain
6+
validation is the verify-release workflow's job.
7+
8+
Purpose: catch the silent-failure pattern in scripts/crossbuild.mk where
9+
the signing block exits 0 without producing a signature (e.g. a future
10+
regression that returns the recipe to /bin/sh and silently skips the
11+
bash conditional).
12+
13+
Usage: python3 scripts/verify_signed.py <path-to-exe>
14+
"""
15+
16+
import struct
17+
import sys
18+
19+
20+
def main(argv: list[str]) -> int:
21+
if len(argv) != 2:
22+
print(
23+
f"usage: {argv[0]} <path-to-exe>",
24+
file=sys.stderr,
25+
)
26+
return 2
27+
path = argv[1]
28+
with open(path, "rb") as f:
29+
data = f.read()
30+
31+
# PE header offset is stored at 0x3C.
32+
pe = struct.unpack_from("<I", data, 0x3C)[0]
33+
# Optional header Magic: 0x10B = PE32, 0x20B = PE32+.
34+
magic = struct.unpack_from("<H", data, pe + 24)[0]
35+
# DataDirectory starts at +96 (PE32) or +112 (PE32+) of optional header.
36+
# Certificate Table is index 4; each entry is 8 bytes (VirtualAddress, Size).
37+
dd_start = pe + 24 + (96 if magic == 0x10B else 112)
38+
cert_size = struct.unpack_from("<I", data, dd_start + 4 * 8 + 4)[0]
39+
40+
if cert_size == 0:
41+
print(
42+
f"ERROR: {path} has no Authenticode signature attached. "
43+
"The signing step in crossbuild.mk silently failed.",
44+
file=sys.stderr,
45+
)
46+
return 1
47+
return 0
48+
49+
50+
if __name__ == "__main__":
51+
sys.exit(main(sys.argv))

provider-ci/test-providers/cloudflare/scripts/crossbuild.mk

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,12 @@ bin/%/$(PROVIDER) bin/%/$(PROVIDER).exe: bin/jsign-7.4.jar
5959
fi; \
6060
fi
6161

62+
@# Verify the windows binary was actually signed when signing was expected.
63+
@# This runs in its own shell as a separate recipe line, so make checks its
64+
@# exit code directly (no set-e-inside-if antipattern). Catches the silent
65+
@# failure where the signing block above exits 0 without producing a signature.
66+
@[ "${GOOS}" != "windows" ] || [ "${SKIP_SIGNING}" = "true" ] || python3 scripts/verify_signed.py "$@"
67+
6268
bin/jsign-7.4.jar:
6369
wget https://github.com/ebourg/jsign/releases/download/7.4/jsign-7.4.jar --output-document=bin/jsign-7.4.jar
6470

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
"""Verify a Windows PE binary has an Authenticode signature attached.
2+
3+
Presence check only: parses the PE Optional Header's Certificate Table
4+
directory entry and exits 0 if it is non-empty, non-zero otherwise. Does
5+
not validate the certificate chain or the signature itself; full chain
6+
validation is the verify-release workflow's job.
7+
8+
Purpose: catch the silent-failure pattern in scripts/crossbuild.mk where
9+
the signing block exits 0 without producing a signature (e.g. a future
10+
regression that returns the recipe to /bin/sh and silently skips the
11+
bash conditional).
12+
13+
Usage: python3 scripts/verify_signed.py <path-to-exe>
14+
"""
15+
16+
import struct
17+
import sys
18+
19+
20+
def main(argv: list[str]) -> int:
21+
if len(argv) != 2:
22+
print(
23+
f"usage: {argv[0]} <path-to-exe>",
24+
file=sys.stderr,
25+
)
26+
return 2
27+
path = argv[1]
28+
with open(path, "rb") as f:
29+
data = f.read()
30+
31+
# PE header offset is stored at 0x3C.
32+
pe = struct.unpack_from("<I", data, 0x3C)[0]
33+
# Optional header Magic: 0x10B = PE32, 0x20B = PE32+.
34+
magic = struct.unpack_from("<H", data, pe + 24)[0]
35+
# DataDirectory starts at +96 (PE32) or +112 (PE32+) of optional header.
36+
# Certificate Table is index 4; each entry is 8 bytes (VirtualAddress, Size).
37+
dd_start = pe + 24 + (96 if magic == 0x10B else 112)
38+
cert_size = struct.unpack_from("<I", data, dd_start + 4 * 8 + 4)[0]
39+
40+
if cert_size == 0:
41+
print(
42+
f"ERROR: {path} has no Authenticode signature attached. "
43+
"The signing step in crossbuild.mk silently failed.",
44+
file=sys.stderr,
45+
)
46+
return 1
47+
return 0
48+
49+
50+
if __name__ == "__main__":
51+
sys.exit(main(sys.argv))

provider-ci/test-providers/docker/scripts/crossbuild.mk

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,12 @@ bin/%/$(PROVIDER) bin/%/$(PROVIDER).exe: bin/jsign-7.4.jar
5959
fi; \
6060
fi
6161

62+
@# Verify the windows binary was actually signed when signing was expected.
63+
@# This runs in its own shell as a separate recipe line, so make checks its
64+
@# exit code directly (no set-e-inside-if antipattern). Catches the silent
65+
@# failure where the signing block above exits 0 without producing a signature.
66+
@[ "${GOOS}" != "windows" ] || [ "${SKIP_SIGNING}" = "true" ] || python3 scripts/verify_signed.py "$@"
67+
6268
bin/jsign-7.4.jar:
6369
wget https://github.com/ebourg/jsign/releases/download/7.4/jsign-7.4.jar --output-document=bin/jsign-7.4.jar
6470

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
"""Verify a Windows PE binary has an Authenticode signature attached.
2+
3+
Presence check only: parses the PE Optional Header's Certificate Table
4+
directory entry and exits 0 if it is non-empty, non-zero otherwise. Does
5+
not validate the certificate chain or the signature itself; full chain
6+
validation is the verify-release workflow's job.
7+
8+
Purpose: catch the silent-failure pattern in scripts/crossbuild.mk where
9+
the signing block exits 0 without producing a signature (e.g. a future
10+
regression that returns the recipe to /bin/sh and silently skips the
11+
bash conditional).
12+
13+
Usage: python3 scripts/verify_signed.py <path-to-exe>
14+
"""
15+
16+
import struct
17+
import sys
18+
19+
20+
def main(argv: list[str]) -> int:
21+
if len(argv) != 2:
22+
print(
23+
f"usage: {argv[0]} <path-to-exe>",
24+
file=sys.stderr,
25+
)
26+
return 2
27+
path = argv[1]
28+
with open(path, "rb") as f:
29+
data = f.read()
30+
31+
# PE header offset is stored at 0x3C.
32+
pe = struct.unpack_from("<I", data, 0x3C)[0]
33+
# Optional header Magic: 0x10B = PE32, 0x20B = PE32+.
34+
magic = struct.unpack_from("<H", data, pe + 24)[0]
35+
# DataDirectory starts at +96 (PE32) or +112 (PE32+) of optional header.
36+
# Certificate Table is index 4; each entry is 8 bytes (VirtualAddress, Size).
37+
dd_start = pe + 24 + (96 if magic == 0x10B else 112)
38+
cert_size = struct.unpack_from("<I", data, dd_start + 4 * 8 + 4)[0]
39+
40+
if cert_size == 0:
41+
print(
42+
f"ERROR: {path} has no Authenticode signature attached. "
43+
"The signing step in crossbuild.mk silently failed.",
44+
file=sys.stderr,
45+
)
46+
return 1
47+
return 0
48+
49+
50+
if __name__ == "__main__":
51+
sys.exit(main(sys.argv))

provider-ci/test-providers/eks/scripts/crossbuild.mk

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,12 @@ bin/%/$(PROVIDER) bin/%/$(PROVIDER).exe: bin/jsign-7.4.jar
5959
fi; \
6060
fi
6161

62+
@# Verify the windows binary was actually signed when signing was expected.
63+
@# This runs in its own shell as a separate recipe line, so make checks its
64+
@# exit code directly (no set-e-inside-if antipattern). Catches the silent
65+
@# failure where the signing block above exits 0 without producing a signature.
66+
@[ "${GOOS}" != "windows" ] || [ "${SKIP_SIGNING}" = "true" ] || python3 scripts/verify_signed.py "$@"
67+
6268
bin/jsign-7.4.jar:
6369
wget https://github.com/ebourg/jsign/releases/download/7.4/jsign-7.4.jar --output-document=bin/jsign-7.4.jar
6470

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
"""Verify a Windows PE binary has an Authenticode signature attached.
2+
3+
Presence check only: parses the PE Optional Header's Certificate Table
4+
directory entry and exits 0 if it is non-empty, non-zero otherwise. Does
5+
not validate the certificate chain or the signature itself; full chain
6+
validation is the verify-release workflow's job.
7+
8+
Purpose: catch the silent-failure pattern in scripts/crossbuild.mk where
9+
the signing block exits 0 without producing a signature (e.g. a future
10+
regression that returns the recipe to /bin/sh and silently skips the
11+
bash conditional).
12+
13+
Usage: python3 scripts/verify_signed.py <path-to-exe>
14+
"""
15+
16+
import struct
17+
import sys
18+
19+
20+
def main(argv: list[str]) -> int:
21+
if len(argv) != 2:
22+
print(
23+
f"usage: {argv[0]} <path-to-exe>",
24+
file=sys.stderr,
25+
)
26+
return 2
27+
path = argv[1]
28+
with open(path, "rb") as f:
29+
data = f.read()
30+
31+
# PE header offset is stored at 0x3C.
32+
pe = struct.unpack_from("<I", data, 0x3C)[0]
33+
# Optional header Magic: 0x10B = PE32, 0x20B = PE32+.
34+
magic = struct.unpack_from("<H", data, pe + 24)[0]
35+
# DataDirectory starts at +96 (PE32) or +112 (PE32+) of optional header.
36+
# Certificate Table is index 4; each entry is 8 bytes (VirtualAddress, Size).
37+
dd_start = pe + 24 + (96 if magic == 0x10B else 112)
38+
cert_size = struct.unpack_from("<I", data, dd_start + 4 * 8 + 4)[0]
39+
40+
if cert_size == 0:
41+
print(
42+
f"ERROR: {path} has no Authenticode signature attached. "
43+
"The signing step in crossbuild.mk silently failed.",
44+
file=sys.stderr,
45+
)
46+
return 1
47+
return 0
48+
49+
50+
if __name__ == "__main__":
51+
sys.exit(main(sys.argv))

0 commit comments

Comments
 (0)