app
KiCad - the KiCad Bridge
Public Made by Adomby adom
Drive your own KiCad from an AI, in the background: open every editor, read the whole board as data (the EDA Bridge Standard board model), stack-up, part heights, netlists, place, route, pour, DRC and exports.
master
Colby Knox
sign_azure: ask the signing host for its LOCALAPPDATA instead of a hard-coded user folder
983cf4f
15d ago
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236
#!/usr/bin/env python3
"""Authenticode-sign kicad-bridge.exe as Adom Industries, Inc. through Azure Trusted Signing, the path every
Adom release uses (adom/altium-bridge scripts/release.py, sign_pe_files).
The signing runs on a Windows host reached through Adom Bridge, under that host's own Azure login. Every
identifier (host, staging folder, endpoint, account, profile, tenant, timestamp URL, expected subject) and the
sign-verify.ps1 script live in a PRIVATE folder outside this public repo: $ADOM_SIGNING_DIR, else
~/.adom/signing/kicad-bridge, else ~/.adom/signing/altium-bridge (the same account signs every Adom bridge).
Nothing from it is ever written to this repo, a reply, a log line or an issue.
python3 tools/sign_azure.py <exe> <version> # sign one exe in place; exits non-zero unless verified
On the signing machine itself (this WSL runs on the config's host) it signs locally through powershell.exe,
staging under the host's %LOCALAPPDATA%; elsewhere it goes through Adom Bridge. The flow, each step a gate:
1. stage the exe and sign-verify.ps1 on the host (send_files; the folder name has no dots);
2. one run_script signs and verifies there: Get-AuthenticodeSignature Valid, the expected subject,
a Microsoft timestamp, signtool verify /pa;
3. pull the signed exe back, check its sha256 matches the host's and differs from the unsigned build;
4. read its Authenticode signer here (openssl): CN = "Adom Industries, Inc.".
"""
import base64
import hashlib
import json
import os
import shutil
import struct
import subprocess
import sys
import tempfile
from pathlib import Path
SIGNER_ORG = "Adom Industries, Inc."
THREAD = "kicad-bridge release signing"
def signing_dir():
env = os.environ.get("ADOM_SIGNING_DIR")
if env:
return Path(env)
for name in ("kicad-bridge", "altium-bridge"):
d = Path.home() / ".adom" / "signing" / name
if (d / "config.json").is_file():
return d
return Path.home() / ".adom" / "signing" / "kicad-bridge"
def authenticode_subjects(path):
"""Certificate subjects in a PE's Authenticode signature, read without Windows: the security data
directory points at a WIN_CERTIFICATE holding PKCS#7 DER. [] means unsigned."""
b = Path(path).read_bytes()
pe = struct.unpack_from("<I", b, 0x3C)[0]
if b[pe:pe + 4] != b"PE\0\0":
raise SystemExit(f"{path}: not a PE file")
opt = pe + 24
magic = struct.unpack_from("<H", b, opt)[0]
dirs = opt + (112 if magic == 0x20B else 96)
off, size = struct.unpack_from("<II", b, dirs + 4 * 8)
if not off or not size:
return []
length, _rev, ctype = struct.unpack_from("<IHH", b, off)
if ctype != 2:
return []
with tempfile.NamedTemporaryFile(suffix=".p7b", delete=False) as f:
f.write(b[off + 8:off + length])
r = subprocess.run(["openssl", "pkcs7", "-inform", "DER", "-in", f.name, "-print_certs", "-noout"], capture_output=True, text=True)
os.unlink(f.name)
return [l[len("subject="):].strip() for l in r.stdout.splitlines() if l.startswith("subject=")]
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} (found {subs or 'no signature'})")
return subs
def ab_json(*args, timeout=600):
out = subprocess.run(["adom-bridge", *args], capture_output=True, text=True, timeout=timeout).stdout
d = json.loads(out[out.find("{"):]) if "{" in out else {}
o = d.get("output")
if isinstance(o, str) and o.strip().startswith("{"):
try:
d = json.loads(o)
except ValueError:
pass
return d
POWERSHELL = "/mnt/c/Windows/System32/WindowsPowerShell/v1.0/powershell.exe"
def redact(text, cfg):
"""A message with every private config value blanked: the signing script's hints name the tenant."""
text = str(text)
for k in ("endpoint", "account", "profile", "tenant", "timestampUrl", "stageDir"):
v = str(cfg.get(k) or "")
if len(v) > 3:
text = text.replace(v, f"<{k}>")
return text
def local_windows_host():
"""This machine's Windows name when we run in WSL on it (the signing machine itself), else None."""
if not os.path.exists(POWERSHELL):
return None
r = subprocess.run(["/mnt/c/Windows/System32/cmd.exe", "/c", "hostname"], capture_output=True, text=True, cwd="/mnt/c")
return r.stdout.strip() or None
def checks_of(v, cfg):
return {
"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 ""),
}
def sign_locally(exe, ver, cfg, ps1):
"""On the signing machine itself (WSL on it): stage under its own %LOCALAPPDATA%, run sign-verify.ps1
with powershell.exe, no Adom Bridge round trip."""
win = lambda p: subprocess.run(["wslpath", "-w", str(p)], capture_output=True, text=True).stdout.strip()
lad = subprocess.run(["/mnt/c/Windows/System32/cmd.exe", "/c", "echo %LOCALAPPDATA%"], capture_output=True, text=True, cwd="/mnt/c").stdout.strip()
stage_win = cfg["stageDir"].replace("%LOCALAPPDATA%", lad) + "\\kicad-build-" + ver.replace(".", "_")
stage = Path(subprocess.run(["wslpath", "-u", stage_win], capture_output=True, text=True).stdout.strip())
shutil.rmtree(stage, ignore_errors=True)
stage.mkdir(parents=True)
unsigned_sha = hashlib.sha256(exe.read_bytes()).hexdigest()
shutil.copy2(exe, stage / "kicad-bridge.exe")
shutil.copy2(ps1, stage / "sign-verify.ps1")
print(f"== sign kicad-bridge.exe {ver} here (the signing machine) ==")
r = subprocess.run([POWERSHELL, "-NoProfile", "-NonInteractive", "-ExecutionPolicy", "Bypass", "-File", win(stage / "sign-verify.ps1"),
"-Exe", win(stage / "kicad-bridge.exe"), "-Endpoint", cfg["endpoint"], "-Account", cfg["account"],
"-Profile", cfg["profile"], "-Tenant", cfg["tenant"], "-Timestamp", cfg["timestampUrl"]],
capture_output=True, text=True, cwd="/mnt/c", timeout=540)
out = r.stdout
try:
v = json.loads(out[out.find("{"):out.rfind("}") + 1])
except ValueError:
raise SystemExit(f"REFUSED: the signing script gave no verdict (exit {r.returncode}): {redact((out + r.stderr)[-600:], cfg)}")
if v.get("error"):
raise SystemExit(f"REFUSED: cannot sign ({v['error']}): {redact(v.get('detail', ''), cfg)}\n fix: {redact(v.get('fix', ''), cfg)}")
checks = checks_of(v, cfg)
if not all(checks.values()):
raise SystemExit(f"REFUSED: signature checks failed: {checks}")
signed = stage / "kicad-bridge.exe"
signed_sha = hashlib.sha256(signed.read_bytes()).hexdigest()
if signed_sha == unsigned_sha:
raise SystemExit("REFUSED: the exe is byte-identical after signing")
subs = assert_signed(signed)
shutil.copy2(signed, exe)
shutil.rmtree(stage, ignore_errors=True)
print(" " + ", ".join(f"{k} {'ok' if ok else 'FAIL'}" for k, ok in checks.items()) + f"; verified again here, sha256 {signed_sha[:12]}")
return {"file": "kicad-bridge.exe", "version": ver, "status": v["status"], "subject": SIGNER_ORG, "timestamper": v["timestamper"],
"signtoolVerify": "Successfully verified", "sha256Unsigned": unsigned_sha, "sha256": signed_sha,
"localSubjects": [x for x in subs if SIGNER_ORG in x], "where": "signing machine (local)"}
def sign(exe, ver):
"""Sign `exe` in place: here when this is the signing machine (WSL on it), else on the signing host
through Adom Bridge. Returns the signing report (no private values in it)."""
exe = Path(exe)
sd = signing_dir()
cfg_p, ps1 = sd / "config.json", sd / "sign-verify.ps1"
if not cfg_p.is_file() or not ps1.is_file():
raise SystemExit(f"REFUSED: private signing config missing at {sd} (config.json + sign-verify.ps1). It is "
f"deliberately not in this public repo; ask Colby to copy it over privately.")
cfg = json.loads(cfg_p.read_text())
host = cfg["host"]
here = local_windows_host()
if here and here.lower() == host.lower():
return sign_locally(exe, ver, cfg, ps1)
lad = cfg.get("localAppData") or (ab_json("--target", host, "--ai-thread", THREAD, "run_script", json.dumps({
"interpreter": "powershell", "scriptB64": base64.b64encode(b"$env:LOCALAPPDATA").decode(), "timeout": 30,
"reason": "read the signing host's LOCALAPPDATA for the staging folder"})).get("stdout") or "").strip()
if not lad:
raise SystemExit("REFUSED: could not read the signing host's LOCALAPPDATA for the staging folder")
stage = cfg["stageDir"].replace("%LOCALAPPDATA%", lad) \
+ "\\kicad-build-" + ver.replace(".", "_") # a dotted folder name reads as a file path to send_files
unsigned_sha = hashlib.sha256(exe.read_bytes()).hexdigest()
work = Path(tempfile.mkdtemp(prefix="kicad-sign-"))
name = "kicad-bridge.exe"
shutil.copy2(exe, work / name)
shutil.copy2(ps1, work / "sign-verify.ps1")
print(f"== sign {name} {ver} on the signing host ==")
sent = ab_json("--target", host, "--ai-thread", THREAD, "send_files", json.dumps({
"filePaths": [str(work / name), str(work / "sign-verify.ps1")], "dest": stage, "destIsFolder": True,
"reason": f"stage kicad-bridge {ver} for Authenticode signing"}))
if int(sent.get("filesReceived") or 0) != 2:
shutil.rmtree(work, ignore_errors=True)
raise SystemExit(f"REFUSED: the signing host did not receive both files: {sent.get('error') or sent.get('_hint') or 'no reply'}")
call = (f"(& '{stage}\\sign-verify.ps1' -Exe '{stage}\\{name}' -Endpoint '{cfg['endpoint']}' -Account '{cfg['account']}' "
f"-Profile '{cfg['profile']}' -Tenant '{cfg['tenant']}' -Timestamp '{cfg['timestampUrl']}' | Out-String)")
cmd = f"$r = @({call}); '[' + ($r -join ',') + ']'"
r = ab_json("--target", host, "--ai-thread", THREAD, "run_script", json.dumps({
"interpreter": "powershell", "scriptB64": base64.b64encode(cmd.encode()).decode(), "timeout": 540,
"reason": f"Authenticode-sign kicad-bridge {ver} as {SIGNER_ORG}, then Get-AuthenticodeSignature + signtool verify"}), timeout=900)
so = r.get("stdout") or ""
try:
v = json.loads(so[so.find("["):])[0]
except (ValueError, IndexError):
shutil.rmtree(work, ignore_errors=True)
raise SystemExit(f"REFUSED: the signing script gave no verdict (stdout {len(so)} chars, stderr {len(r.get('stderr') or '')} chars, error {r.get('errorCode') or r.get('error') or '-'})")
if v.get("error"):
shutil.rmtree(work, ignore_errors=True)
raise SystemExit(f"REFUSED: the signing host cannot sign ({v['error']}): {redact(v.get('detail', ''), cfg)}\n fix: {redact(v.get('fix', ''), cfg)}")
checks = checks_of(v, cfg)
if not all(checks.values()):
shutil.rmtree(work, ignore_errors=True)
raise SystemExit(f"REFUSED: signature checks failed on the host: {checks}")
pulled = ab_json("--target", host, "--ai-thread", THREAD, "pull_file", json.dumps({
"filePaths": [f"{stage}\\{name}"], "saveTo": str(work / "back"), "reason": f"pull the signed kicad-bridge {ver} back for packaging"}))
files = pulled.get("files") or []
if not files:
shutil.rmtree(work, ignore_errors=True)
raise SystemExit(f"REFUSED: could not pull the signed exe back: {pulled.get('error') or 'no files'}")
back = Path(files[0]["path"])
signed_sha = hashlib.sha256(back.read_bytes()).hexdigest()
if v.get("sha256") and signed_sha != v["sha256"].lower():
raise SystemExit("REFUSED: the signed exe changed in transit (host and local sha256 differ)")
if signed_sha == unsigned_sha:
raise SystemExit("REFUSED: the pulled-back exe is byte-identical to the unsigned build")
subs = assert_signed(back)
shutil.copy2(back, exe)
shutil.rmtree(work, ignore_errors=True)
report = {"file": name, "version": ver, "status": v["status"], "subject": SIGNER_ORG, "timestamper": v["timestamper"],
"signtoolVerify": "Successfully verified", "sha256Unsigned": unsigned_sha, "sha256": signed_sha,
"localSubjects": [s for s in subs if SIGNER_ORG in s]}
print(" " + ", ".join(f"{k} {'ok' if ok else 'FAIL'}" for k, ok in checks.items()) + f"; verified again here, sha256 {signed_sha[:12]}")
return report
if __name__ == "__main__":
if len(sys.argv) != 3:
sys.exit(__doc__)
print(json.dumps(sign(sys.argv[1], sys.argv[2]), indent=2))