From 6546e700bee1a4dd6c4aa302c814c5d54453835e Mon Sep 17 00:00:00 2001 From: Tian Gao Date: Wed, 22 Nov 2023 13:27:43 -0800 Subject: [PATCH 1/3] Add python tools for package/isolate --- tools/packaging/README.md | 69 ++++++++++++++ tools/packaging/isolate.py | 177 +++++++++++++++++++++++++++++++++++ tools/packaging/package.py | 74 +++++++++++++++ tools/packaging/template.xml | 126 +++++++++++++++++++++++++ 4 files changed, 446 insertions(+) create mode 100644 tools/packaging/README.md create mode 100644 tools/packaging/isolate.py create mode 100644 tools/packaging/package.py create mode 100644 tools/packaging/template.xml diff --git a/tools/packaging/README.md b/tools/packaging/README.md new file mode 100644 index 0000000..7f4f355 --- /dev/null +++ b/tools/packaging/README.md @@ -0,0 +1,69 @@ +# Command line tools to package and isolate applications + +## Package + +You can convert your `msi` or `exe` installers to `msix` using `package.py`. + +### Requirement + +* Python3 +* MSIX Packaging Tool +* A [template](https://learn.microsoft.com/en-us/windows/msix/packaging-tool/generate-template-file) file. + You can also use the [example](./template.xml) we give as a start + +### Usage + +#### Prepare your template + +The best way to generate a template is to use MSIX Packaging Tool to [package](../../docs/packaging/msix-packaging-tool.md#win32---msix) +your application once and save the template. + +However, you can also fill in the template manually. The most important sections are `` and `` + +*Note: you have to fill the `PublisherName` field of `` accurately (matching your cert) in order to sign the package* + +#### Run the script + +``` +python package.py --template template.xml -o app.msix installer.msi +``` + +Under the hood, this is very similar to using +[MSIX Packaging Command Line Tool](https://learn.microsoft.com/en-us/windows/msix/packaging-tool/package-conversion-command-line) + +The script helps you fill the `` and `` according to your input, but feel free to use `MsixPackagingTool.exe` +directly. + +*Note: this step requires an elevation because `MsixPackagingTool.exe` needs it* + +#### Finish the installation + +You need to go though the installation of the application. To make sure this works properly, the app should be uninstalled first +if it's already installed. + +## Isolate + +You can isolate your `msix` package using `isolate.py`. + +### Requirement + +* Python3 +* makeappx.exe +* signtool.exe +* Windows version >= 25357 +* `.pfx` certification + +### Usage + +``` +python isolate.py --cert your_cert.pfx -o isolated.msix app.msix +``` + +The command will try to use the `makeappx.exe` and `signtool.exe` in your enviroment. However, if they do not exist in `PATH` +or you want to use a specific version of them, you can pass the executable you want to use by `--makeappx` and `--signtool` + +In order to add capabilities, use `--capability` or `--cap` like + +``` +python isolate.py --cert your_cert.pfx -o isolated.msix --cap runFullTrust --cap isolatedWin32-promptForAccess app.msix +``` \ No newline at end of file diff --git a/tools/packaging/isolate.py b/tools/packaging/isolate.py new file mode 100644 index 0000000..81c3554 --- /dev/null +++ b/tools/packaging/isolate.py @@ -0,0 +1,177 @@ +import argparse +import os +import platform +import re +import subprocess +import tempfile +from dataclasses import dataclass + + +@dataclass +class IsolateConfig: + output: str + cert: str + signtool: str + makeappx: str + working_dir: str + msix: str + capabilities: list[str] + + +class Manifest: + def __init__(self, path: str, config: IsolateConfig): + self.path = path + self.config = config + with open(path, "r") as f: + self.str = f.read() + + def modify_package(self, s: str): + m = re.search(r"", s, re.MULTILINE | re.DOTALL) + if m: + package = m.group(0) + if "IgnorableNamespaces" not in package: + package = package.replace(">", ' IgnorableNamespaces="">') + + if "xmlns:previewsecurity2=" not in package: + package = package.replace(">", ' xmlns:previewsecurity2="http://schemas.microsoft.com/appx/manifest/preview/windows10/security/2">') + package = package.replace('IgnorableNamespaces="', 'IgnorableNamespaces="previewsecurity2 ') + + if "xmlns:uap10=" not in package: + package = package.replace(">", ' xmlns:uap10="http://schemas.microsoft.com/appx/manifest/uap/windows10/10">') + package = package.replace('IgnorableNamespaces="', 'IgnorableNamespaces="uap10 ') + + if "xmlns:rescap=" not in package: + package = package.replace(">", ' xmlns:rescap="http://schemas.microsoft.com/appx/manifest/foundation/windows10/restrictedcapabilities">') + package = package.replace('IgnorableNamespaces="', 'IgnorableNamespaces="rescap ') + + s = s.replace(m.group(0), package) + return s + else: + raise ValueError("No package found in manifest") + + def modify_target_device_family(self, s: str): + m = re.search(r"", s, re.MULTILINE | re.DOTALL) + if m: + target_device_family = m.group(0) + s = s.replace(target_device_family, '') + return s + + def modify_application(self, s: str): + for m in re.finditer(r"", s, re.MULTILINE | re.DOTALL): + application = m.group(0) + application = re.sub('EntryPoint=".*?"', "", application) + application = re.sub(' .*?TrustLevel=".*?"', "", application) + application = re.sub(' .*?RuntimeBehavior=".*?"', "", application) + application = application.replace(">", ' uap10:TrustLevel="appContainer" previewsecurity2:RuntimeBehavior="appSilo">') + s = s.replace(m.group(0), application) + return s + + def modify_capabilities(self, s: str): + m = re.search(r".*?", s, re.MULTILINE | re.DOTALL) + if m: + capabilities = m.group(0) + capabilities = re.sub(r'', '', capabilities) + for capability in self.config.capabilities: + capabilities = capabilities.replace("", f'\n') + s = s.replace(m.group(0), capabilities) + elif self.config.capabilities: + capabilities = '\n' + for capability in self.config.capabilities: + capabilities = capabilities.replace("", f'\n') + s = s.replace("", capabilities + "") + return s + + def process(self): + self.str = self.modify_package(self.str) + self.str = self.modify_target_device_family(self.str) + self.str = self.modify_application(self.str) + self.str = self.modify_capabilities(self.str) + + def save(self, path=None): + if not path: + path = self.path + with open(path, "w") as f: + f.write(self.str) + + +def unpack(config: IsolateConfig): + print("Unpacking...") + # Create working directory + os.makedirs(config.working_dir, exist_ok=True) + + # Extract the package + subprocess.check_call(f"{config.makeappx} unpack /p {config.msix} /d {os.path.join(config.working_dir, 'unpack')}", shell=True) + + +def pack_and_sign(config: IsolateConfig): + print("Repacking...") + # Repack the package + subprocess.check_call(f"{config.makeappx} pack /nv /d {os.path.join(config.working_dir, 'unpack')} /p {config.output}", shell=True) + + # Sign the package + subprocess.check_call(f"{config.signtool} sign /fd SHA256 /f {config.cert} {config.output}", shell=True) + + +def modify_manifest(config: IsolateConfig): + manifest = Manifest(os.path.join(config.working_dir, "unpack", "AppxManifest.xml"), config) + manifest.process() + manifest.save() + + +def check_requirements(args): + if platform.system() != "Windows": + print("This script only works on Windows") + exit(1) + + if int(platform.version().split(".")[2]) < 25357: + print("WARNING! This script might not work on your Windows version. You need at least 10.0.25357") + + try: + subprocess.call([args.signtool, "/?"], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) + except FileNotFoundError: + print(f"{args.signtool} is not found, pass your signtool.exe path with --signtool") + exit(1) + + try: + subprocess.call([args.makeappx, "/?"], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) + except FileNotFoundError: + print(f"{args.makeappx} is not found, pass your makeappx.exe path with --makeappx") + exit(1) + + for capability in args.capability: + if capability != "runFullTrust" and not capability.startswith("isolatedWin32"): + print(f"Invalid capability: {capability}. Only runFullTrust and isolatedWin32-* are supported") + exit(1) + + +def main(): + parser = argparse.ArgumentParser() + parser.add_argument("--output", "-o", default=None) + parser.add_argument("--cert", "-c", required=True) + parser.add_argument("--capability", "--cap", action="append", default=[]) + parser.add_argument("--signtool", default="signtool.exe") + parser.add_argument("--makeappx", default="makeappx.exe") + parser.add_argument("msix") + + args = parser.parse_args() + + check_requirements(args) + + with tempfile.TemporaryDirectory() as tmpdir: + config = IsolateConfig( + output=os.path.abspath(args.output), + cert=args.cert, + signtool=args.signtool, + makeappx=args.makeappx, + working_dir=os.path.abspath(tmpdir), + msix=os.path.abspath(args.msix) if args.msix else None, + capabilities=args.capability, + ) + + unpack(config) + modify_manifest(config) + pack_and_sign(config) + + +if __name__ == "__main__": + main() diff --git a/tools/packaging/package.py b/tools/packaging/package.py new file mode 100644 index 0000000..a2591b4 --- /dev/null +++ b/tools/packaging/package.py @@ -0,0 +1,74 @@ +import argparse +import os +import subprocess +import tempfile +import xml.etree.ElementTree as XMLET +from dataclasses import dataclass + + +@dataclass +class PackageConfig: + template: str + output: str + working_dir: str + installer: str + + +def make_msix(config: PackageConfig): + ns = { + "V1": "http://schemas.microsoft.com/appx/msixpackagingtool/template/2018", + "V2": "http://schemas.microsoft.com/appx/msixpackagingtool/template/1904", + "V3": "http://schemas.microsoft.com/appx/msixpackagingtool/template/1907", + "V4": "http://schemas.microsoft.com/appx/msixpackagingtool/template/1910", + "V5": "http://schemas.microsoft.com/appx/msixpackagingtool/template/2001", + } + tree = XMLET.parse(config.template) + root = tree.getroot() + + # Set the save location for template + save_location = root.find("V1:SaveLocation", ns) + if not save_location: + save_location = XMLET.Element(f"{{{ns['V1']}}}SaveLocation") + save_location.set("PackagePath", config.output) + root.append(save_location) + + # Set the installer to use + installer = root.find("V1:Installer", ns) + if not installer: + if not config.installer: + print("No installer is passed, please pass your installer or add it to the template") + exit(1) + installer = XMLET.Element(f"{{{ns['V1']}}}Installer") + installer.set(f"Path", config.installer) + if config.installer.endswith(".exe"): + installer.set(f"Arguments", "/qn /norestart INSTALLSTARTMENUSHORTCUTS=1 DISABLEADVTSHORTCUTS=1") + root.append(installer) + + template_path = os.path.join(config.working_dir, "template.xml") + tree.write(template_path) + + # Create the package + subprocess.call(f"MsixPackagingTool.exe create-package --template {template_path}", shell=True) + + +def main(): + parser = argparse.ArgumentParser() + parser.add_argument("--template", "-t", required=True) + parser.add_argument("--output", "-o", default="app.msix") + parser.add_argument("installer", nargs="?") + + args = parser.parse_args() + + with tempfile.TemporaryDirectory() as tmpdir: + config = PackageConfig( + template=args.template, + output=os.path.abspath(args.output), + working_dir=os.path.abspath(tmpdir), + installer=os.path.abspath(args.installer) if args.installer else None, + ) + + make_msix(config) + + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/tools/packaging/template.xml b/tools/packaging/template.xml new file mode 100644 index 0000000..570b68d --- /dev/null +++ b/tools/packaging/template.xml @@ -0,0 +1,126 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + From 3c18ff1a3561674cf5c0fd3a030d06cec17ba655 Mon Sep 17 00:00:00 2001 From: Tian Gao Date: Tue, 12 Dec 2023 12:40:54 -0800 Subject: [PATCH 2/3] Find the sdk dir automatically --- tools/packaging/isolate.py | 61 +++++++++++++++++++++++++------------- 1 file changed, 40 insertions(+), 21 deletions(-) diff --git a/tools/packaging/isolate.py b/tools/packaging/isolate.py index 81c3554..37ab0a1 100644 --- a/tools/packaging/isolate.py +++ b/tools/packaging/isolate.py @@ -2,6 +2,7 @@ import os import platform import re +import shutil import subprocess import tempfile from dataclasses import dataclass @@ -100,16 +101,16 @@ def unpack(config: IsolateConfig): os.makedirs(config.working_dir, exist_ok=True) # Extract the package - subprocess.check_call(f"{config.makeappx} unpack /p {config.msix} /d {os.path.join(config.working_dir, 'unpack')}", shell=True) + subprocess.check_call([config.makeappx, "unpack", "/p", config.msix, "/d", os.path.join(config.working_dir, "unpack")], shell=True) def pack_and_sign(config: IsolateConfig): print("Repacking...") # Repack the package - subprocess.check_call(f"{config.makeappx} pack /nv /d {os.path.join(config.working_dir, 'unpack')} /p {config.output}", shell=True) + subprocess.check_call([config.makeappx, "pack", "/nv", "/d", os.path.join(config.working_dir, 'unpack'), "/p" ,config.output], shell=True) # Sign the package - subprocess.check_call(f"{config.signtool} sign /fd SHA256 /f {config.cert} {config.output}", shell=True) + subprocess.check_call([config.signtool, "sign", "/fd", "SHA256", "/f", config.cert, config.output], shell=True) def modify_manifest(config: IsolateConfig): @@ -118,24 +119,43 @@ def modify_manifest(config: IsolateConfig): manifest.save() +def find_sdk_dir(tmpdir): + """ + Find the SDK dir associated with MSIX Packaging Tool and copy it to tmpdir + We need to copy it because we don't have access to execute it in the original location + """ + stdout = subprocess.check_output(["powershell.exe", "Get-AppxPackage", "-name", "Microsoft.MSIXPackagingTool"]) + + match = re.search(r"InstallLocation\s+:\s+(.*)", stdout.decode("utf-8")) + if match is None: + return None + original_sdk_dir = os.path.join(match.group(1).strip(), "SDK") + sdk_dir = os.path.join(tmpdir, "SDK") + shutil.copytree(original_sdk_dir, sdk_dir) + return sdk_dir + + def check_requirements(args): if platform.system() != "Windows": print("This script only works on Windows") exit(1) - if int(platform.version().split(".")[2]) < 25357: - print("WARNING! This script might not work on your Windows version. You need at least 10.0.25357") + if args.sdk_dir is None: + print("MSIX Packaging Tool is not installed, please either install it or pass your SDK directory with --sdk_dir") + exit(1) try: - subprocess.call([args.signtool, "/?"], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) + subprocess.call([os.path.join(args.sdk_dir, "signtool.exe"), "/?"], + stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, shell=True) except FileNotFoundError: - print(f"{args.signtool} is not found, pass your signtool.exe path with --signtool") + print(f"signtool.exe is not found in {args.sdk_dir}") exit(1) try: - subprocess.call([args.makeappx, "/?"], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) + subprocess.call([os.path.join(args.sdk_dir, "makeappx.exe"), "/?"], + stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, shell=True) except FileNotFoundError: - print(f"{args.makeappx} is not found, pass your makeappx.exe path with --makeappx") + print(f"makeappx.exe is not found in {args.sdk_dir}") exit(1) for capability in args.capability: @@ -145,24 +165,23 @@ def check_requirements(args): def main(): - parser = argparse.ArgumentParser() - parser.add_argument("--output", "-o", default=None) - parser.add_argument("--cert", "-c", required=True) - parser.add_argument("--capability", "--cap", action="append", default=[]) - parser.add_argument("--signtool", default="signtool.exe") - parser.add_argument("--makeappx", default="makeappx.exe") - parser.add_argument("msix") + with tempfile.TemporaryDirectory() as tmpdir: + parser = argparse.ArgumentParser() + parser.add_argument("--output", "-o", default=None) + parser.add_argument("--cert", "-c", required=True) + parser.add_argument("--capability", "--cap", action="append", default=[]) + parser.add_argument("--sdk_dir", default=find_sdk_dir(tmpdir)) + parser.add_argument("msix") - args = parser.parse_args() + args = parser.parse_args() - check_requirements(args) + check_requirements(args) - with tempfile.TemporaryDirectory() as tmpdir: config = IsolateConfig( output=os.path.abspath(args.output), cert=args.cert, - signtool=args.signtool, - makeappx=args.makeappx, + signtool=os.path.join(args.sdk_dir, "signtool.exe"), + makeappx=os.path.join(args.sdk_dir, "makeappx.exe"), working_dir=os.path.abspath(tmpdir), msix=os.path.abspath(args.msix) if args.msix else None, capabilities=args.capability, From a72aad36d5ffee485cae5da870ccb764f4bcef5e Mon Sep 17 00:00:00 2001 From: Tian Gao Date: Fri, 1 Mar 2024 14:07:28 -0800 Subject: [PATCH 3/3] Add python scripts for packaging and isolation --- tools/packaging/README.md | 13 ++++++------- tools/packaging/isolate.py | 2 +- tools/packaging/template.xml | 2 +- 3 files changed, 8 insertions(+), 9 deletions(-) diff --git a/tools/packaging/README.md b/tools/packaging/README.md index 7f4f355..34e518b 100644 --- a/tools/packaging/README.md +++ b/tools/packaging/README.md @@ -48,10 +48,8 @@ You can isolate your `msix` package using `isolate.py`. ### Requirement * Python3 -* makeappx.exe -* signtool.exe -* Windows version >= 25357 -* `.pfx` certification +* [MSIX Packaging Tool](https://github.com/microsoft/win32-app-isolation/releases) +* `.pfx` certification to sign your package ### Usage @@ -59,11 +57,12 @@ You can isolate your `msix` package using `isolate.py`. python isolate.py --cert your_cert.pfx -o isolated.msix app.msix ``` -The command will try to use the `makeappx.exe` and `signtool.exe` in your enviroment. However, if they do not exist in `PATH` -or you want to use a specific version of them, you can pass the executable you want to use by `--makeappx` and `--signtool` +The command will try to use the `makeappx.exe` and `signtool.exe` from your MSIX Packaging Tool. +If you want to use your own SDK version, use `--sdk_dir` to pass the directory that has the +binaries. In order to add capabilities, use `--capability` or `--cap` like ``` python isolate.py --cert your_cert.pfx -o isolated.msix --cap runFullTrust --cap isolatedWin32-promptForAccess app.msix -``` \ No newline at end of file +``` diff --git a/tools/packaging/isolate.py b/tools/packaging/isolate.py index 37ab0a1..c4e6c27 100644 --- a/tools/packaging/isolate.py +++ b/tools/packaging/isolate.py @@ -167,7 +167,7 @@ def check_requirements(args): def main(): with tempfile.TemporaryDirectory() as tmpdir: parser = argparse.ArgumentParser() - parser.add_argument("--output", "-o", default=None) + parser.add_argument("--output", "-o", default=None, required=True) parser.add_argument("--cert", "-c", required=True) parser.add_argument("--capability", "--cap", action="append", default=[]) parser.add_argument("--sdk_dir", default=find_sdk_dir(tmpdir)) diff --git a/tools/packaging/template.xml b/tools/packaging/template.xml index 570b68d..107cc53 100644 --- a/tools/packaging/template.xml +++ b/tools/packaging/template.xml @@ -94,7 +94,7 @@