← Commit history

1.0.53: 1.0.53: the first SIGNED native build. kicad-bridge.exe is signed as Adom Industries, Inc. with a Microsoft timestamp, so Windows 11 Smart App Control no longer blocks it (every native build up to 1.0.52 shipped unsigned). Same code as 1.0.52. The release tool now signs the exe as its last change, checks the exe inside the zip is the signed one, and refuses to publish an unsigned build.

John Lauer ·b60b3f1c96 ·9d ago ·parent b007dc0
1 file changed +33
tools/build_native_release.py+33
@@ -32,6 +32,16 @@ PLATFORMS = {     "linux": {"supported": False, "notes": "Barrett's build: cargo build --release on Ubuntu, crates/kicad-platform/src/linux.rs."}, } +def authenticode_size(path):+    """Size of the PE certificate table (the Authenticode signature); 0 = unsigned."""+    import struct+    b = Path(path).read_bytes()+    pe = struct.unpack_from("<I", b, 0x3C)[0]+    magic = struct.unpack_from("<H", b, pe + 24)[0]+    dd = pe + 24 + (96 if magic == 0x10B else 112)+    return struct.unpack_from("<II", b, dd + 32)[1]++ def sh(cmd, cwd=None):     r = subprocess.run(cmd, cwd=cwd, text=True, capture_output=True)     if r.returncode != 0:@@ -102,6 +112,21 @@ def main():     dist = RUST / "dist"; dist.mkdir(exist_ok=True)     stage = dist / "stage"; shutil.rmtree(stage, ignore_errors=True); stage.mkdir()     shutil.copy2(exe, stage / "kicad-bridge.exe")+    # SIGNING, the last change to the exe (anything after it would break the signature). Windows 11+    # Smart App Control blocks unsigned exes, so a published build is ALWAYS signed as Adom+    # Industries, Inc.; the signer is a private tool outside this public repo (ADOM_SIGN_EXE,+    # default ~/bin/adom-sign-exe). Every native build up to 1.0.52 shipped unsigned.+    signed = False+    if publish or "--sign" in sys.argv:+        signer = os.environ.get("ADOM_SIGN_EXE") or os.path.expanduser("~/bin/adom-sign-exe")+        if not os.path.isfile(signer):+            sys.exit(f"REFUSING: no exe signer at {signer}; an unsigned exe is never published")+        r = subprocess.run([signer, str(stage / "kicad-bridge.exe"), "--bridge", "kicad-bridge"], text=True)+        if r.returncode != 0:+            sys.exit("REFUSING: signing failed (see above); nothing was published")+        signed = authenticode_size(stage / "kicad-bridge.exe") > 0+        if not signed:+            sys.exit("REFUSING: the signer reported success but the exe carries no signature")     (stage / "bridge.json").write_text(json.dumps(manifest, indent=2) + "\n")     (stage / "BRIDGE_VERSION").write_text(ver + "\n")     # Non-code assets the Python zip shipped and the native verbs still read at run time:@@ -123,6 +148,14 @@ def main():             if f.is_file():                 z.write(f, f.relative_to(stage).as_posix())     sha = hashlib.sha256(zpath.read_bytes()).hexdigest()+    if publish:+        # check what actually ships: the exe INSIDE the zip must be the signed one+        with zipfile.ZipFile(zpath) as z:+            inner = dist / "stage-check.exe"; inner.write_bytes(z.read("kicad-bridge.exe"))+        ok = authenticode_size(inner) > 0; inner.unlink()+        if not ok:+            sys.exit("REFUSING: the exe inside the zip is unsigned; nothing was published")+    print(f"exe signed: {'yes (Adom Industries, Inc.)' if signed else 'NO (local build only; --publish signs)'}")     print(f"built {zpath.name}\n  verbs : {len(verbs)}\n  exe   : {exe.stat().st_size} bytes\n  zip   : {zpath.stat().st_size} bytes\n  sha256: {sha}")     if not publish:         print("not published (pass --publish --notes \"...\" to ship to insiders)")