-
-
Notifications
You must be signed in to change notification settings - Fork 902
[Bounty #632] Code sign the Windows releases #1304
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,93 @@ | ||
| """Windows code signing support for ActivityWatch releases. | ||
|
|
||
| Addresses issue #632: Code sign the Windows releases | ||
| """ | ||
|
|
||
| import os | ||
| import subprocess | ||
| import logging | ||
| from pathlib import Path | ||
| from typing import Optional | ||
|
|
||
| logger = logging.getLogger(__name__) | ||
|
|
||
|
|
||
| class WindowsCodeSigner: | ||
| """Handles code signing for Windows releases.""" | ||
|
|
||
| def __init__( | ||
| self, | ||
| certificate_path: Optional[str] = None, | ||
| certificate_password: Optional[str] = None, | ||
| timestamp_server: str = "http://timestamp.digicert.com", | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
| tool: str = "auto", | ||
| ): | ||
| self.certificate_path = certificate_path or os.environ.get("CODESIGN_CERT_PATH") | ||
| self.certificate_password = certificate_password or os.environ.get("CODESIGN_CERT_PASSWORD") | ||
| self.timestamp_server = timestamp_server | ||
| self.tool = self._detect_tool(tool) | ||
|
|
||
| def _detect_tool(self, tool: str) -> str: | ||
| if tool != "auto": | ||
| return tool | ||
| for t in ["signtool", "osslsigncode"]: | ||
| try: | ||
| subprocess.run([t, "--help"], capture_output=True, check=True) | ||
| return t | ||
| except (subprocess.CalledProcessError, FileNotFoundError): | ||
| continue | ||
| logger.warning("No code signing tool found") | ||
| return "none" | ||
|
Comment on lines
+33
to
+40
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
|
|
||
| def sign_file(self, filepath: str) -> bool: | ||
| if self.tool == "none" or not self.certificate_path: | ||
| logger.error("Signing not configured") | ||
| return False | ||
| if not Path(filepath).exists(): | ||
| logger.error(f"File not found: {filepath}") | ||
| return False | ||
| if self.tool == "signtool": | ||
| cmd = ["signtool", "sign", "/f", self.certificate_path, "/t", self.timestamp_server, filepath] | ||
| if self.certificate_password: | ||
| cmd.extend(["/p", self.certificate_password]) | ||
| elif self.tool == "osslsigncode": | ||
| output = filepath + ".signed" | ||
| cmd = ["osslsigncode", "sign", "-pkcs12", self.certificate_path, "-t", self.timestamp_server, "-in", filepath, "-out", output] | ||
| if self.certificate_password: | ||
| cmd.extend(["-pass", self.certificate_password]) | ||
|
Comment on lines
+51
to
+57
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
The certificate password is placed directly in the subprocess argument list ( |
||
| else: | ||
| return False | ||
| try: | ||
| subprocess.run(cmd, capture_output=True, text=True, check=True) | ||
| logger.info(f"Successfully signed: {filepath}") | ||
| return True | ||
| except subprocess.CalledProcessError as e: | ||
| logger.error(f"Signing failed: {e.stderr}") | ||
| return False | ||
|
Comment on lines
+53
to
+66
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
After |
||
|
|
||
| def sign_directory(self, directory: str, extensions: tuple = (".exe", ".dll", ".msi")) -> dict: | ||
| results = {"signed": [], "failed": [], "skipped": []} | ||
| for root, dirs, files in os.walk(directory): | ||
| for f in files: | ||
| fp = os.path.join(root, f) | ||
| if f.endswith(extensions): | ||
| if self.sign_file(fp): | ||
| results["signed"].append(fp) | ||
| else: | ||
| results["failed"].append(fp) | ||
| else: | ||
| results["skipped"].append(fp) | ||
| return results | ||
|
|
||
| def verify_signature(self, filepath: str) -> bool: | ||
| if self.tool == "signtool": | ||
| cmd = ["signtool", "verify", "/pa", filepath] | ||
| elif self.tool == "osslsigncode": | ||
| cmd = ["osslsigncode", "verify", "-in", filepath] | ||
| else: | ||
| return False | ||
| try: | ||
| subprocess.run(cmd, capture_output=True, text=True, check=True) | ||
| return True | ||
| except subprocess.CalledProcessError: | ||
| return False | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The new
WindowsCodeSignerclass has no call site in the existing build workflow. Thebuild.ymlWindows job installs Inno Setup and packages the release, but never imports or invokes this module. The macOS equivalent (codesign/xcnotary) is called inline inbuild.ymlunder the "Package dmg" step. Without a corresponding "Package Windows" step that callssign_directory, this script will never actually sign any release artefact.