← Commit history

sign_azure: sign locally when this is the signing machine; redact private values in every message

Colby Knox ·55ef80b41c ·15d ago ·parent 47c0d03
1 file changed +79−9
tools/sign_azure.py+79−9
@@ -10,7 +10,8 @@ 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 -The flow, each step a gate:+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;@@ -85,8 +86,79 @@ def ab_json(*args, timeout=600):     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 on the signing host; returns the signing report (no private values in it)."""+    """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"@@ -95,6 +167,9 @@ def sign(exe, ver):                          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)     stage = cfg["stageDir"].replace("%LOCALAPPDATA%", cfg.get("localAppData", r"C:\Users\cknox\AppData\Local")) \         + "\\kicad-build-" + ver.replace(".", "_")  # a dotted folder name reads as a file path to send_files     unsigned_sha = hashlib.sha256(exe.read_bytes()).hexdigest()@@ -123,13 +198,8 @@ def sign(exe, ver):         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']}): {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 ""),-    }+        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}")