← Commit history

0.2.44 (STAGED, signed exe + dll): release.py signs and gates every PE file the zip ships, including the Altium extension DLL (issue 15)

Colby Knox ·4ebc65e1a3 ·22d ago ·parent 3c91649
Co-Authored-By: Claude Opus 5.5 <[email protected]>
8 files changed +93−60
CHANGELOG.md+4
@@ -1,5 +1,9 @@ # Changelog +## 0.2.44 (2026-09-29)+- **The Altium extension DLL is signed too.** `extension/AdomBridge/AdomBridge.dll` now carries the same Adom Industries, Inc. signature as `altium-bridge.exe`: Windows Smart App Control checks the DLLs a program loads, not only the exe it starts, so an unsigned plugin could be refused on a PC where it is on. The release script signs and verifies every exe and DLL it ships and refuses to package any that is unsigned. Code otherwise identical to 0.2.43.++ ## 0.2.43 (2026-09-29) - **The exe is signed.** `altium-bridge.exe` now carries an Authenticode signature from Adom Industries, Inc., with a timestamp, so Windows 11 Smart App Control no longer blocks it ("Part of this app has been blocked"). The release script signs every exe it ships as the last step before packaging, verifies the signature on Windows and again after the file comes back, and refuses to package an unsigned exe. - Same code as 0.2.42 otherwise (bounded `altium_panel`, `altium_demo` error reporting), which is still waiting for its licensed test run.
bridge-rs/src/main.rs+1−1
@@ -29,7 +29,7 @@ use tiny_http::{Header, Method, Response, Server}; /// Attached to every failed reply (see the /command handler). const REPORT_ISSUES: &str = "If this looks like an altium-bridge BUG (not a refusal you expected), report it: check for a duplicate with `adom-wiki issue list adom/altium-bridge`, then `adom-wiki issue create adom/altium-bridge --title \"<verb>: <one-line symptom>\" --category bug-report --body \"<what you ran, what you expected, what happened, the VERBATIM response JSON (and errorShot if present), bridgeVersion, the Altium version from altium_status, the OS>\"`. A relay, routing, toast or lifecycle problem belongs on adom/adom-bridge instead. Reports from real machines are how this bridge gets correct: three different Altium builds behaved three different ways in one weekend."; -pub const BRIDGE_VERSION: &str = "0.2.43";+pub const BRIDGE_VERSION: &str = "0.2.44";  fn json_response(status: u16, body: &serde_json::Value) -> Response<std::io::Cursor<Vec<u8>>> {     let data = serde_json::to_vec(body).unwrap_or_else(|_| b"{}".to_vec());
package.json+1−1
@@ -1,6 +1,6 @@ {   "slug": "altium-bridge",-  "version": "0.2.43",+  "version": "0.2.44",   "type": "app",   "description": "Skills for your container so your AI knows how to drive the Altium Designer bridge (altium_* verbs). The bridge runtime itself is the release zip; Adom Bridge loads that.",   "tags": [
page.json+1−1
@@ -4,7 +4,7 @@   "type": "app",   "title": "Altium - the Altium Bridge",   "brief": "Adom Bridge for Altium Designer: a resident DelphiScript agent gives the AI eval, the 1,100-command process launcher, all 8 EDA surfaces with screenshots, compile/ERC, DRC, OutJob outputs, native SchLib/PcbLib authoring with STEP 3D bodies, and dialog handling by PID.",-  "version": "0.2.43",+  "version": "0.2.44",   "hero": {     "type": "image",     "path": "screenshots/hero.png"
scripts/release.py+69−55
@@ -18,13 +18,13 @@ pattern: adom/insiders skills + adom/pup-bridge tools/ship.sh, which did it firs   python3 scripts/release.py --stage "<log>"       # build, sign, upload the release asset ONLY: no tier,                                                    #    no skills pkg (a build waiting for a live battery). -SIGNING: every .exe the zip ships is Authenticode-signed as "Adom Industries, Inc." before-packaging (Windows Smart App Control blocks unsigned exes). The signing identifiers live in a+SIGNING: every PE file the zip ships (.exe AND .dll) is Authenticode-signed as "Adom Industries, Inc." before+packaging (Windows Smart App Control blocks unsigned exes, and unsigned DLLs a program loads). The signing identifiers live in a PRIVATE config outside this public repo ($ADOM_SIGNING_DIR, default ~/.adom/signing/altium-bridge: config.json + sign-verify.ps1); the exe is signed on the Windows host that config names, through that host's signer login, verified there (Get-AuthenticodeSignature + signtool verify /pa), pulled-back with a hash check, and verified AGAIN here. build_zip refuses any unsigned .exe, so no path-through this script can package one. Signing is the last change to the file before packaging.+back with a hash check, and verified AGAIN here. build_zip refuses any unsigned .exe or .dll, so no+path through this script can package one. Signing is the last change to the file before packaging.  Rules (insiders-tier-publish, verbatim where it matters): every publish is insiders; public only on a human's words; an AI never promotes on its own judgment; one plain semver per build in both@@ -69,7 +69,7 @@ INCLUDE = [     ("resources/adom-overlay-32.png", SRC / "resources" / "adom-overlay-32.png"),     ("extension/Install-AdomBridge.ps1", SRC / "extension" / "Install-AdomBridge.ps1"),     ("extension/REMOVE-AdomBridge.cmd", SRC / "extension" / "REMOVE-AdomBridge.cmd"),-    ("extension/AdomBridge/AdomBridge.dll", SRC / "extension" / "AdomBridge" / "AdomBridge.dll"),+    ("extension/AdomBridge/AdomBridge.dll", DIST / "signed" / "AdomBridge.dll"),     ("extension/AdomBridge/AdomBridge.ins", SRC / "extension" / "AdomBridge" / "AdomBridge.ins"),     ("extension/AdomBridge/AdomBridge.rcs", SRC / "extension" / "AdomBridge" / "AdomBridge.rcs"),     ("extension/AdomBridge/Help/ServerDlls.txt", SRC / "extension" / "AdomBridge" / "Help" / "ServerDlls.txt"),@@ -194,8 +194,8 @@ def assert_signed(path):     subs = authenticode_subjects(path)     if not any(f'CN = "{SIGNER_ORG}"' in s for s in subs):         raise SystemExit(f"REFUSED: {path} is not Authenticode-signed by {SIGNER_ORG} "-                         f"(found {subs or 'no signature'}). Unsigned exes are blocked by Windows Smart App Control; "-                         f"this script never packages one.")+                         f"(found {subs or 'no signature'}). Windows Smart App Control blocks unsigned exes and the "+                         f"unsigned DLLs a program loads; this script never packages one.")     return subs  @@ -211,8 +211,16 @@ def ab_json(*args, timeout=600):     return d  -def sign_exe(ver):-    """Sign EXE on the private config's Windows host, verify there, pull it back, verify here."""+# (published name, unsigned source). Each is signed on the host, then shipped from dist/signed/.+PE_SOURCES = [+    ("altium-bridge.exe", EXE),+    ("AdomBridge.dll", SRC / "extension" / "AdomBridge" / "AdomBridge.dll"),+]+++def sign_pe_files(ver):+    """Sign every PE file the zip ships on the private config's Windows host, verify each there,+    pull them back, verify each here."""     import base64     sd = signing_dir()     cfg_p, ps1 = sd / "config.json", sd / "sign-verify.ps1"@@ -221,61 +229,67 @@ def sign_exe(ver):                          f"It is deliberately not in this public repo; ask the release owner for it.")     cfg = json.loads(cfg_p.read_text())     host, thread = cfg["host"], "altium-bridge release signing"-    stage = cfg["stageDir"].replace("%LOCALAPPDATA%", cfg.get("localAppData", r"C:\Users\cknox\AppData\Local")) + "\\build-" + ver.replace(".", "_")  # a dotted name reads as a file path to send_files+    stage = cfg["stageDir"].replace("%LOCALAPPDATA%", cfg.get("localAppData", r"C:\Users\cknox\AppData\Local")) \+        + "\\build-" + ver.replace(".", "_")  # a dotted name reads as a file path to send_files     work = DIST / "signing"     shutil.rmtree(work, ignore_errors=True)     work.mkdir(parents=True)-    unsigned = work / "altium-bridge.exe"-    shutil.copy2(EXE, unsigned)+    for name, src in PE_SOURCES:+        shutil.copy2(src, work / name)     shutil.copy2(ps1, work / "sign-verify.ps1")-    print(f"== sign {unsigned.name} on the signing host ==")+    names = [n for n, _ in PE_SOURCES]+    print(f"== sign {', '.join(names)} on the signing host ==")     sent = ab_json("--target", host, "--ai-thread", thread, "send_files", json.dumps({-        "filePaths": [str(unsigned), str(work / "sign-verify.ps1")], "dest": stage,+        "filePaths": [str(work / n) for n in names] + [str(work / "sign-verify.ps1")], "dest": stage,         "reason": f"stage altium-bridge {ver} for Authenticode signing"}))-    if int(sent.get("filesReceived") or 0) != 2:+    if int(sent.get("filesReceived") or 0) != len(names) + 1:         raise SystemExit(f"signing host did not receive the files: {sent.get('error') or sent}")-    win_exe = stage + "\\altium-bridge.exe"-    cmd = (f"& '{stage}\\sign-verify.ps1' -Exe '{win_exe}' -Endpoint '{cfg['endpoint']}' "-           f"-Account '{cfg['account']}' -Profile '{cfg['profile']}' -Tenant '{cfg['tenant']}' "-           f"-Timestamp '{cfg['timestampUrl']}'")+    calls = "; ".join(+        f"(& '{stage}\\sign-verify.ps1' -Exe '{stage}\\{n}' -Endpoint '{cfg['endpoint']}' -Account '{cfg['account']}' "+        f"-Profile '{cfg['profile']}' -Tenant '{cfg['tenant']}' -Timestamp '{cfg['timestampUrl']}' | Out-String)"+        for n in names)+    cmd = f"$r = @({calls}); '[' + ($r -join ',') + ']'"     r = ab_json("--target", host, "--ai-thread", thread, "run_script", json.dumps({-        "interpreter": "powershell", "scriptB64": base64.b64encode(cmd.encode()).decode(), "timeout": 300,-        "reason": f"Authenticode-sign altium-bridge {ver} as {SIGNER_ORG}, then Get-AuthenticodeSignature + signtool verify"}))+        "interpreter": "powershell", "scriptB64": base64.b64encode(cmd.encode()).decode(), "timeout": 540,+        "reason": f"Authenticode-sign altium-bridge {ver} ({', '.join(names)}) as {SIGNER_ORG}, then Get-AuthenticodeSignature + signtool verify"}), timeout=900)     so = r.get("stdout") or ""     try:-        v = json.loads(so[so.find("{"):])+        verdicts = json.loads(so[so.find("["):])     except ValueError:         raise SystemExit(f"signing script gave no verdict: {so[:400]} {(r.get('stderr') or '')[:400]}")-    if v.get("error"):-        raise SystemExit(f"REFUSED: signing host cannot sign ({v['error']}): {v.get('detail', '')}\n  fix: {v.get('fix', '')}")-    checks = {-        "Status Valid": v.get("status") == "Valid",-        "signer subject": cfg["expectSubject"] in (v.get("subject") or ""),-        "timestamped": "Microsoft" in (v.get("timestamper") or ""),-        "signtool verify /pa": "Successfully verified" in (v.get("signtoolVerify") or ""),-    }-    if not all(checks.values()):-        raise SystemExit(f"REFUSED: signature checks failed on the signing host: {checks}\n{json.dumps(v, indent=1)}")-    pulled = ab_json("--target", host, "--ai-thread", thread, "pull_file", json.dumps({-        "filePaths": [win_exe], "reason": f"pull the signed altium-bridge {ver} exe back for packaging"}))-    files = pulled.get("files") or []-    if not files:-        raise SystemExit(f"could not pull the signed exe back: {pulled.get('error') or pulled}")-    signed = DIST / "signed" / "altium-bridge.exe"-    signed.parent.mkdir(parents=True, exist_ok=True)-    shutil.move(files[0]["path"], signed)-    local_sha = hashlib.sha256(signed.read_bytes()).hexdigest()-    if local_sha != v.get("sha256"):-        raise SystemExit(f"REFUSED: signed exe changed in transit (host {v.get('sha256')}, here {local_sha})")-    subs = assert_signed(signed)-    report = {"file": "altium-bridge.exe", "status": v["status"], "subject": v["subject"],-              "timestamper": v["timestamper"], "signtoolVerify": v["signtoolVerify"],-              "signer": v.get("signer"), "sha256": local_sha, "localSubjects": subs}-    (DIST / "signed" / "signing-report.json").write_text(json.dumps(report, indent=2) + "\n")-    for k, ok in checks.items():-        print(f"   {k}: {'ok' if ok else 'FAIL'}")-    print(f"   verified again here: {SIGNER_ORG}, sha256 {local_sha[:12]}")-    return report+    reports = []+    for name, v in zip(names, verdicts):+        if v.get("error"):+            raise SystemExit(f"REFUSED: signing host cannot sign ({v['error']}): {v.get('detail', '')}\n  fix: {v.get('fix', '')}")+        checks = {+            "Status Valid": v.get("status") == "Valid",+            "signer subject": cfg["expectSubject"] in (v.get("subject") or ""),+            "timestamped": "Microsoft" in (v.get("timestamper") or ""),+            "signtool verify /pa": "Successfully verified" in (v.get("signtoolVerify") or ""),+        }+        if not all(checks.values()):+            raise SystemExit(f"REFUSED: signature checks failed for {name}: {checks}\n{json.dumps(v, indent=1)}")+        pulled = ab_json("--target", host, "--ai-thread", thread, "pull_file", json.dumps({+            "filePaths": [f"{stage}\\{name}"], "reason": f"pull the signed {name} ({ver}) back for packaging"}))+        files = pulled.get("files") or []+        if not files:+            raise SystemExit(f"could not pull the signed {name} back: {pulled.get('error') or pulled}")+        signed = DIST / "signed" / name+        signed.parent.mkdir(parents=True, exist_ok=True)+        shutil.move(files[0]["path"], signed)+        local_sha = hashlib.sha256(signed.read_bytes()).hexdigest()+        if local_sha != v.get("sha256"):+            raise SystemExit(f"REFUSED: {name} changed in transit (host {v.get('sha256')}, here {local_sha})")+        subs = assert_signed(signed)+        reports.append({"file": name, "status": v["status"], "subject": v["subject"],+                        "timestamper": v["timestamper"], "signtoolVerify": v["signtoolVerify"].split(":")[0],+                        "signer": v.get("signer"), "sha256": local_sha, "localSubjects": subs})+        print(f"   {name}: " + ", ".join(f"{k} {'ok' if ok else 'FAIL'}" for k, ok in checks.items())+              + f"; verified again here, sha256 {local_sha[:12]}")+    if len(reports) != len(names):+        raise SystemExit(f"REFUSED: expected {len(names)} verdicts, got {len(reports)}")+    (DIST / "signed" / "signing-report.json").write_text(json.dumps(reports, indent=2) + "\n")+    return reports   def build_zip(ver):@@ -283,7 +297,7 @@ def build_zip(ver):     zname = f"adom-bridge-altium-v{ver}.zip"     zpath = DIST / zname     for rel, p in INCLUDE:-        if rel.lower().endswith(".exe"):+        if rel.lower().endswith((".exe", ".dll")):             assert_signed(p)     with zipfile.ZipFile(zpath, "w", zipfile.ZIP_DEFLATED) as z:         for rel, p in INCLUDE:@@ -339,7 +353,7 @@ def stage_only(log_line):     n = lint_verbs()     print(f"verb lint ok: {n} verbs")     build_exe()-    sign_exe(ver)+    sign_pe_files(ver)     zname, zpath, sha = build_zip(ver)     subprocess.run(["adom-wiki", "release", "create", PAGE, ver, "--title", f"altium-bridge {ver}",                     "--changelog", f"STAGED, not on any tier: {log_line}"], check=False, capture_output=True)@@ -356,7 +370,7 @@ def ship_insiders(log_line):     n = lint_verbs()     print(f"verb lint ok: {n} verbs declared and dispatchable")     build_exe()-    sign_exe(ver)+    sign_pe_files(ver)     zname, zpath, sha = build_zip(ver)      print(f"== release {ver} on {PAGE} (bytes live on the product page, both tiers) ==")
scripts/releases/0.2.44.jsonadded+15
@@ -0,0 +1,15 @@+{+  "name": "altium",+  "version": "0.2.44",+  "url": "https://wiki.adom.inc/download/adom/altium-bridge/0.2.44/adom-bridge-altium-v0.2.44.zip",+  "sha256": "e4e7b0e3872e76225a6a6ed2ba6ff75cbd8582488d1441a78f6c135786afd904",+  "size": 1658739,+  "verbPrefixes": [+    "altium_"+  ],+  "healthEndpoint": "/health",+  "updateManifestUrl": "https://wiki.adom.inc/api/v1/pages/altium-bridge/files/adom-bridge-altium-manifest.json",+  "insidersManifestUrl": "https://wiki.adom.inc/api/v1/pages/insiders/files/altium-bridge/version.json",+  "released_at": "2026-09-29T20:40:39.610700Z",+  "tier": "insiders"+}
src/BRIDGE_VERSION+1−1
@@ -1 +1 @@-0.2.43+0.2.44
src/bridge.json+1−1
@@ -2,7 +2,7 @@   "manifest_version": 1,   "name": "altium",   "displayName": "Altium Designer",-  "version": "0.2.43",+  "version": "0.2.44",   "description": "Drive Altium Designer from Adom: a resident DelphiScript agent (eval + the 1,100-command process launcher), the X2 -R command line, UIA over the VCL shell, dialogs by PID, screenshots of every Altium window, and Altium 365.",   "homepage": "https://wiki.adom.inc/adom/altium-bridge",   "docs": "https://wiki.adom.inc/adom/altium-bridge",