Files
max-sixty__worktrunk/.github/verify-windows-signature.py
Maximilian Roos f1f1b50d85 docs(release): trim the signing archaeology now the path is proven (#3593)
Follow-up to #3590, now that v0.69.2 has shipped a signed Windows binary
— verified against the published asset rather than the logs:

```
git-wt.exe: certificate table 8480 bytes
wt.exe:     certificate table 8472 bytes
c4343fb5…1a43 *worktrunk-x86_64-pc-windows-msvc.zip   (published .sha256 matches)
```

Comments only; no behavior change.

The signing comments were written while the mechanism was still being
guessed at, so they carried the failed attempts — a ten-line account of
the zip-of-a-zip failure, a six-line account of the `…zip.zip`
collision. The path is proven and the workflow now verifies itself, so
what a reader needs is the current mechanism. Trimmed to that.

One thing added rather than removed. `Recompute checksum` sits between
two `continue-on-error` steps and looks like it should fold into the
replace step above it. It must not: the signing steps are tolerant
because publishing can't depend on a self-signed test certificate, while
a checksum that doesn't match the shipped zip has to fail the release.
That asymmetry is invisible in the YAML, so it's now stated — the next
person to simplify this shouldn't have to rediscover it.

> _This was written by Claude Code on behalf of max_
2026-07-25 03:22:07 -07:00

48 lines
1.8 KiB
Python

"""Report whether every executable in the Windows release zip is signed.
The signing chain breaks silently — v0.69.0 and v0.69.1 both shipped unsigned
under a green run, two different ways — so this reads the zip itself and
nothing but the published bytes can satisfy it.
It checks that a certificate is present, not that it chains to a trusted root:
SignPath is on a self-signed test certificate pending the project's OSS
application, which is also why the caller runs this with continue-on-error.
Usage: python .github/verify-windows-signature.py <zip>
"""
import struct
import sys
import zipfile
def certificate_table_size(pe: bytes) -> int:
"""Size of the PE's certificate table — data directory entry 4, zero when unsigned."""
pe_header = struct.unpack_from("<I", pe, 0x3C)[0]
optional_header = pe_header + 24
magic = struct.unpack_from("<H", pe, optional_header)[0]
# Data directories follow the optional header, whose size differs between
# PE32 (magic 0x10b) and PE32+ (0x20b).
data_directories = optional_header + (96 if magic == 0x10B else 112)
_, size = struct.unpack_from("<II", pe, data_directories + 4 * 8)
return size
archive = sys.argv[1]
unsigned = []
with zipfile.ZipFile(archive) as zf:
executables = [name for name in zf.namelist() if name.endswith(".exe")]
if not executables:
print(f"::error::{archive} contains no .exe to verify")
sys.exit(1)
for name in executables:
size = certificate_table_size(zf.read(name))
print(f"{name}: certificate table {size} bytes")
if size == 0:
unsigned.append(name)
if unsigned:
print(f"::error::unsigned in {archive}: {', '.join(unsigned)}")
sys.exit(1)
print(f"{archive}: all {len(executables)} executables signed")