mirror of
https://github.com/max-sixty/worktrunk.git
synced 2026-09-14 20:00:38 +08:00
f1f1b50d85
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_
48 lines
1.8 KiB
Python
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")
|