From 49a95ac32d472eff9a718045c0f91d6c241744a5 Mon Sep 17 00:00:00 2001 From: Damon Tivel Date: Sun, 30 Mar 2025 17:22:08 -0700 Subject: [PATCH 01/17] Add ClickOnce signing algorithm spec --- docs/specs/ClickOnce-Signing-Algorithm.md | 134 ++++++++++++++++++++++ docs/specs/images/file-relationships.gif | Bin 0 -> 29303 bytes 2 files changed, 134 insertions(+) create mode 100644 docs/specs/ClickOnce-Signing-Algorithm.md create mode 100644 docs/specs/images/file-relationships.gif diff --git a/docs/specs/ClickOnce-Signing-Algorithm.md b/docs/specs/ClickOnce-Signing-Algorithm.md new file mode 100644 index 00000000..45144931 --- /dev/null +++ b/docs/specs/ClickOnce-Signing-Algorithm.md @@ -0,0 +1,134 @@ +# ClickOnce Signing Algorithm + +ClickOnce signing has been the source of numerous bugs, primarily because of fragile assumptions in Sign CLI's ClickOnce signing algorithm. + +## Overview of a ClickOnce application + +A ClickOnce application consists of: + +* a deployment manifest: a ClickOnce `.application` or `.vsto` file. +* an application manifest: a ClickOnce `.manifest` file, not to be confused with a [side-by-side or fusion manifest file](https://learn.microsoft.com/windows/win32/sbscs/application-manifests) with the same extension. +* payload files: assemblies and other files required by the application. +* a bootstrapper: a `setup.exe` file for installing the ClickOnce application. + +Publishing a ClickOnce application generates a bootstrapper, deployment and application manifest, and payload files. The application manifest and payload files are published to a versioned directory, and the deployment manifest is updated to point to the new application manifest. The bootstrapper points to the deployment manifest. + +![ClickOnce file relationships](images/file-relationships.gif) + +## Problem + +Sign CLI's algorithm for signing ClickOnce applications is a source of bugs because of these fragile assumptions: + +- The directory containing the deployment manifest file contains a single ClickOnce application version. In reality, this directory can be the parent directory for many versions of the same ClickOnce application and/or the parent directory for many different ClickOnce applications. +- The directory containing the deployment manifest file contains at most one `.manifest` file in the directory tree. This assumption overlaps with the previous assumption, but even if the directory only contains a single ClickOnce application version, the application may contain multiple `.manifest` files (i.e.: an application manifest and one or more side-by-side manifests). + +The impact of these failed assumptions is that the algorithm is subject to over-copying, over-signing, failing to sign ClickOnce applications containing a side-by-side manifest, and difficulty batch signing multiple ClickOnce applications. + +There are two special cases that complicate signing: + +1. VSTO publishing [signs the deployment manifest then copies it to the versioned application manifest file directory](https://devdiv.visualstudio.com/DevDiv/_git/VS?path=/src/ConfigData/BuildTargets/Microsoft.VisualStudio.Tools.Office.targets&version=GCba009548f0f1014f78b861e34bf4ef2700a28d25&line=473&lineEnd=483&lineStartColumn=9&lineEndColumn=11&lineStyle=plain&_a=contents), presumably for archival purposes. The current algorithm will discover each deployment manifest file and, in separate operations, attempt to sign each manifest and its dependencies. +1. A [comment](https://github.com/dotnet/sign/blob/e268c46059ae415749de057a14c8919c6f063049/src/Sign.Core/DataFormatSigners/ClickOnceSigner.cs#L88-L90) in the existing implementation says: + + ```C# + // It's possible that there might not actually be a .manifest file or any data files if the user just + // wants to re-sign an existing deployment manifest because e.g. the update URL has changed but nothing + // else has. In that case we don't need to touch the other files and we can just sign the deployment manifest. + ``` + +## Proposed solution + +Given a deployment manifest file as a starting point, the algorithm will be updated to: + +1. resolve the local path for the application manifest using information in the deployment manifest +1. resolve the local path of payload files using information in the application manifest +1. resolve the local path of the bootstrapper in the same directory as the deployment manifest +1. copy and sign only these files in the order listed: + - payload files + - application manifest + - deployment manifest + - bootstrapper + +Special cases will be made for VSTO deployment manifests. + +- If step \#1 above succeeds, then the signed deployment manifest will be copied to the versioned application manifest file directory. +- If step \#1 above fails, it will be assumed that the deployment manifest file is in the versioned application manifest file directory and will be skipped for signing. + +## Open questions + +1. To handle the special case of re-signing only the deployment manifest file, it's unclear how we would reliably distinguish that case from the copied `.vsto` file in the versioned application manifest file directory. + * How are CLI arguments identical between whole application and single file signing? + * Is single file deployment manifest file signing done in place (with a reachable application manifest file) or in isolation from other files? + +## Appendix A: Current algorithm + +In a temporary directory: + +1. [[source](https://github.com/dotnet/sign/blob/e268c46059ae415749de057a14c8919c6f063049/src/Sign.Core/Signer.cs#L135-L147)] Copy the deployment manifest to a random file name with the same file extension (`.application` or `.vsto`). +1. [[source](https://github.com/dotnet/sign/blob/e268c46059ae415749de057a14c8919c6f063049/src/Sign.Core/DataFormatSigners/ClickOnceSigner.cs#L261-L274)] Copy all files from the deployment manifest's source directory and all its subdirectories to the temporary directory, while preserving the source's directory structure. _Because copying does not filter down to manifests and payload files, this step can result in overcopying._ +1. [[source](https://github.com/dotnet/sign/blob/e268c46059ae415749de057a14c8919c6f063049/src/Sign.Core/DataFormatSigners/ClickOnceSigner.cs#L97-L113)] Sign all `.deploy` and `.exe` files included by user's file matching patterns. _Previous overcopying can lead to oversigning in this step._ +1. [[source](https://github.com/dotnet/sign/blob/e268c46059ae415749de057a14c8919c6f063049/src/Sign.Core/DataFormatSigners/ClickOnceSigner.cs#L115-L123)] Remove the `.deploy` extension on any remaining files _excluded_ by file matching patterns. While these files may not be signed, they're still necessary to update the application manifest. +1. [[source](https://github.com/dotnet/sign/blob/e268c46059ae415749de057a14c8919c6f063049/src/Sign.Core/DataFormatSigners/ClickOnceSigner.cs#L130-L139)] Find files with the `.manifest` file extension. + * If there are none, continue without signing application manifest. + * If there is exactly one, assume it is the application manifest and sign it. + * If there are multiple files, fail. _This can happen because of earlier overcopying or because side-by-side manifests are not ignored._ +1. [[source](https://github.com/dotnet/sign/blob/e268c46059ae415749de057a14c8919c6f063049/src/Sign.Core/DataFormatSigners/ClickOnceSigner.cs#L155-L183)] Sign all deployment manifests in file path length order descending. _Previous overcopying can lead to oversigning in this step._ +1. [[source](https://github.com/dotnet/sign/blob/e268c46059ae415749de057a14c8919c6f063049/src/Sign.Core/DataFormatSigners/ClickOnceSigner.cs#L155-L183)] Restore `.deploy` extensions. +1. [[source](https://github.com/dotnet/sign/blob/e268c46059ae415749de057a14c8919c6f063049/src/Sign.Core/DataFormatSigners/ClickOnceSigner.cs#L186-L189)] Copy files from the temporary directory back to the source location. _Previous overcopying can lead to overcopying in this step._ + +Here are two examples of how the current algorithm overcopies and oversigns. + +* With the layout as described in [this comment](https://github.com/dotnet/sign/issues/681#issuecomment-2426793329), the current algorithm would sign every version of the application, instead of just the version referenced by App.application: + + ``` + App.application + Application Files + App_1_0_0_0 + App.dll.deploy + App.dll.manifest + App.exe.deploy + ... + App_1_0_1_0 + App.dll.deploy + App.dll.manifest + App.exe.deploy + ... + ... + ... + ``` + +* With the layout as described in [this comment](https://github.com/dotnet/sign/issues/681#issuecomment-2425548289), each deployment manifest and payload file would be signed _n_ times, where _n_ is the number of `.vsto` files. + + ``` + Output + myAddin.Word.dll + myAddin.PowerPoint.dll + myAddin.Excel.dll + + myAddin.Word.vsto + myAddin.PowerPoint.vsto + myAddin.Excel.vsto + + myAddin.Word.dll.manifest + myAddin.PowerPoint.dll.manifest + myAddin.Excel.dll.manifest + ``` + +## Appendix B: Proposed algorithm + +1. If a file has a `.vsto` or `.application` file extension, read it as a deployment manifest using [`ManifestReader.ReadManifest(...)`](https://learn.microsoft.com/dotnet/api/microsoft.build.tasks.deployment.manifestutilities.manifestreader.readmanifest?view=msbuild-17-netcore#microsoft-build-tasks-deployment-manifestutilities-manifestreader-readmanifest(system-io-stream-system-boolean)). If file reading fails or the returned [`Manifest`](https://learn.microsoft.com/dotnet/api/microsoft.build.tasks.deployment.manifestutilities.manifest?view=msbuild-17-netcore) instance is not a [`DeployManifest`](https://learn.microsoft.com/dotnet/api/microsoft.build.tasks.deployment.manifestutilities.deploymanifest?view=msbuild-17-netcore), defer to next signer. +1. Set [`Manifest.ReadOnly`](https://learn.microsoft.com/dotnet/api/microsoft.build.tasks.deployment.manifestutilities.manifest.readonly?view=msbuild-17-netcore) to `true` to ensure read-only mode. +1. Use [`Manifest.ResolveFiles()`](https://learn.microsoft.com/dotnet/api/microsoft.build.tasks.deployment.manifestutilities.manifest.resolvefiles?view=msbuild-17-netcore#microsoft-build-tasks-deployment-manifestutilities-manifest-resolvefiles) to resolve paths. This method: + > Locates all specified assembly and file references by searching in the same directory as the loaded manifest, or in the current directory. The location of each referenced assembly and file is required for hash computation and assembly identity resolution. Any resulting errors or warnings are reported in the OutputMessages collection. +1. Log all messages in [`Manifest.OutputMessages`](https://learn.microsoft.com/dotnet/api/microsoft.build.tasks.deployment.manifestutilities.manifest.outputmessages?view=msbuild-17-netcore). +1. If `Manifest.OutputMessages` contains any errors, fail signing. +1. Obtain the full path of the application manifest file from [`DeployManifest.EntryPoint`](https://learn.microsoft.com/dotnet/api/microsoft.build.tasks.deployment.manifestutilities.deploymanifest.entrypoint?view=msbuild-17-netcore)[`.ResolvedPath`](https://learn.microsoft.com/dotnet/api/microsoft.build.tasks.deployment.manifestutilities.basereference.resolvedpath?view=msbuild-17-netcore#microsoft-build-tasks-deployment-manifestutilities-basereference-resolvedpath). +1. If the application manifest file does not exist, log a warning and stop further processing of the deployment manifest file. +1. Read the application manifest file using `ManifestReader.ReadManifest(...)`. +1. Set `Manifest.ReadOnly` to `true` to ensure read-only mode. +1. Interate through both [`AssemblyReferences`](https://learn.microsoft.com/dotnet/api/microsoft.build.tasks.deployment.manifestutilities.manifest.assemblyreferences?view=msbuild-17-netcore) and [`FileReferences`](https://learn.microsoft.com/dotnet/api/microsoft.build.tasks.deployment.manifestutilities.manifest.filereferences?view=msbuild-17-netcore), manually resolve `TargetPath` property to the base path of the application manifest file. + * Note: it seems like calling `Manifest.ResolveFiles()` would resolve the full file path for every file dependency in the application manifest. However, this fails because `ResolveFiles()` assumes dependency files do not have `.deploy` file extension, but they do. We could temporarily remove the `.deploy` extension and then call `ResolveFiles()` but a user's glob patterns might filter out files based on the `.deploy` extension. The safest option is to resolve file paths ourselves. +1. Copy all files from the previous step, including the application manifest file itself, to a temporary directory. +1. Sign files in the following order: files alongside the application manifest, the application manifest itself, then the deployment manifest. +1. Copy the files back. +1. If the signed deployment manifest file is a `.vsto` file, copy it to the versioned application manifest file directory and overwrite if necessary. + diff --git a/docs/specs/images/file-relationships.gif b/docs/specs/images/file-relationships.gif new file mode 100644 index 0000000000000000000000000000000000000000..c40c5068288e1d26d54cf26a63e8dfa2ee3932b8 GIT binary patch literal 29303 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 Date: Wed, 2 Apr 2025 10:06:50 -0700 Subject: [PATCH 02/17] Apply feedback --- docs/specs/ClickOnce-Signing-Algorithm.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/specs/ClickOnce-Signing-Algorithm.md b/docs/specs/ClickOnce-Signing-Algorithm.md index 45144931..700db8a7 100644 --- a/docs/specs/ClickOnce-Signing-Algorithm.md +++ b/docs/specs/ClickOnce-Signing-Algorithm.md @@ -128,7 +128,7 @@ Here are two examples of how the current algorithm overcopies and oversigns. 1. Interate through both [`AssemblyReferences`](https://learn.microsoft.com/dotnet/api/microsoft.build.tasks.deployment.manifestutilities.manifest.assemblyreferences?view=msbuild-17-netcore) and [`FileReferences`](https://learn.microsoft.com/dotnet/api/microsoft.build.tasks.deployment.manifestutilities.manifest.filereferences?view=msbuild-17-netcore), manually resolve `TargetPath` property to the base path of the application manifest file. * Note: it seems like calling `Manifest.ResolveFiles()` would resolve the full file path for every file dependency in the application manifest. However, this fails because `ResolveFiles()` assumes dependency files do not have `.deploy` file extension, but they do. We could temporarily remove the `.deploy` extension and then call `ResolveFiles()` but a user's glob patterns might filter out files based on the `.deploy` extension. The safest option is to resolve file paths ourselves. 1. Copy all files from the previous step, including the application manifest file itself, to a temporary directory. -1. Sign files in the following order: files alongside the application manifest, the application manifest itself, then the deployment manifest. +1. Sign files in the following order: files alongside the application manifest, the application manifest itself, the deployment manifest, then `setup.exe`. 1. Copy the files back. 1. If the signed deployment manifest file is a `.vsto` file, copy it to the versioned application manifest file directory and overwrite if necessary. From 668913f30b186ed7f31a209f690e0445eec35eee Mon Sep 17 00:00:00 2001 From: Damon Tivel Date: Sun, 27 Apr 2025 15:46:36 -0700 Subject: [PATCH 03/17] Update --- docs/specs/ClickOnce-Signing-Algorithm.md | 40 +++++++++-------------- 1 file changed, 16 insertions(+), 24 deletions(-) diff --git a/docs/specs/ClickOnce-Signing-Algorithm.md b/docs/specs/ClickOnce-Signing-Algorithm.md index 700db8a7..3b3cad69 100644 --- a/docs/specs/ClickOnce-Signing-Algorithm.md +++ b/docs/specs/ClickOnce-Signing-Algorithm.md @@ -22,7 +22,7 @@ Sign CLI's algorithm for signing ClickOnce applications is a source of bugs beca - The directory containing the deployment manifest file contains a single ClickOnce application version. In reality, this directory can be the parent directory for many versions of the same ClickOnce application and/or the parent directory for many different ClickOnce applications. - The directory containing the deployment manifest file contains at most one `.manifest` file in the directory tree. This assumption overlaps with the previous assumption, but even if the directory only contains a single ClickOnce application version, the application may contain multiple `.manifest` files (i.e.: an application manifest and one or more side-by-side manifests). -The impact of these failed assumptions is that the algorithm is subject to over-copying, over-signing, failing to sign ClickOnce applications containing a side-by-side manifest, and difficulty batch signing multiple ClickOnce applications. +The impact is that the algorithm is subject to over-copying, over-signing, failing to sign ClickOnce applications containing a side-by-side manifest, and difficulty batch signing multiple ClickOnce applications. There are two special cases that complicate signing: @@ -39,25 +39,18 @@ There are two special cases that complicate signing: Given a deployment manifest file as a starting point, the algorithm will be updated to: -1. resolve the local path for the application manifest using information in the deployment manifest -1. resolve the local path of payload files using information in the application manifest -1. resolve the local path of the bootstrapper in the same directory as the deployment manifest -1. copy and sign only these files in the order listed: +1. Resolve the local path for the application manifest using information in the deployment manifest. If the application manifest cannot be found, skip to step 3. In step 4, only the deployment manifest and bootstrapper will be signed. +1. Resolve the local path of payload files using information in the application manifest. +1. Resolve the local path of the bootstrapper in the same directory as the deployment manifest. +1. Copy and sign only these files in the order listed: - payload files - application manifest - deployment manifest - bootstrapper -Special cases will be made for VSTO deployment manifests. +The proposed solution will not attempt to mirror VSTO publishing and copy a signed deployment manifest into the application manifest directory. -- If step \#1 above succeeds, then the signed deployment manifest will be copied to the versioned application manifest file directory. -- If step \#1 above fails, it will be assumed that the deployment manifest file is in the versioned application manifest file directory and will be skipped for signing. - -## Open questions - -1. To handle the special case of re-signing only the deployment manifest file, it's unclear how we would reliably distinguish that case from the copied `.vsto` file in the versioned application manifest file directory. - * How are CLI arguments identical between whole application and single file signing? - * Is single file deployment manifest file signing done in place (with a reachable application manifest file) or in isolation from other files? +To handle the special case of re-signing only the deployment manifest file, a user must either exclude other files (e.g.: using glob patterns) or isolate the deployment manifest file so that the application manifest file is not in the expected location relative to the deployment manifest file. ## Appendix A: Current algorithm @@ -115,20 +108,19 @@ Here are two examples of how the current algorithm overcopies and oversigns. ## Appendix B: Proposed algorithm -1. If a file has a `.vsto` or `.application` file extension, read it as a deployment manifest using [`ManifestReader.ReadManifest(...)`](https://learn.microsoft.com/dotnet/api/microsoft.build.tasks.deployment.manifestutilities.manifestreader.readmanifest?view=msbuild-17-netcore#microsoft-build-tasks-deployment-manifestutilities-manifestreader-readmanifest(system-io-stream-system-boolean)). If file reading fails or the returned [`Manifest`](https://learn.microsoft.com/dotnet/api/microsoft.build.tasks.deployment.manifestutilities.manifest?view=msbuild-17-netcore) instance is not a [`DeployManifest`](https://learn.microsoft.com/dotnet/api/microsoft.build.tasks.deployment.manifestutilities.deploymanifest?view=msbuild-17-netcore), defer to next signer. +1. If a file has a `.vsto` or `.application` file extension, read it as a deployment manifest using [`ManifestReader.ReadManifest(...)`](https://learn.microsoft.com/dotnet/api/microsoft.build.tasks.deployment.manifestutilities.manifestreader.readmanifest?view=msbuild-17-netcore#microsoft-build-tasks-deployment-manifestutilities-manifestreader-readmanifest(system-io-stream-system-boolean)). If file reading fails or the returned [`Manifest`](https://learn.microsoft.com/dotnet/api/microsoft.build.tasks.deployment.manifestutilities.manifest?view=msbuild-17-netcore) instance is not a [`DeployManifest`](https://learn.microsoft.com/dotnet/api/microsoft.build.tasks.deployment.manifestutilities.deploymanifest?view=msbuild-17-netcore), stop further processing; nothing will be signed. 1. Set [`Manifest.ReadOnly`](https://learn.microsoft.com/dotnet/api/microsoft.build.tasks.deployment.manifestutilities.manifest.readonly?view=msbuild-17-netcore) to `true` to ensure read-only mode. -1. Use [`Manifest.ResolveFiles()`](https://learn.microsoft.com/dotnet/api/microsoft.build.tasks.deployment.manifestutilities.manifest.resolvefiles?view=msbuild-17-netcore#microsoft-build-tasks-deployment-manifestutilities-manifest-resolvefiles) to resolve paths. This method: - > Locates all specified assembly and file references by searching in the same directory as the loaded manifest, or in the current directory. The location of each referenced assembly and file is required for hash computation and assembly identity resolution. Any resulting errors or warnings are reported in the OutputMessages collection. +1. Use [`Manifest.ResolveFiles(string[])`](https://learn.microsoft.com/dotnet/api/microsoft.build.tasks.deployment.manifestutilities.manifest.resolvefiles?view=msbuild-17-netcore#microsoft-build-tasks-deployment-manifestutilities-manifest-resolvefiles(system-string())) with the directory path of the deployment manifest file. If the application manifest cannot be located, the application manifest and payload files will be skipped for signing. 1. Log all messages in [`Manifest.OutputMessages`](https://learn.microsoft.com/dotnet/api/microsoft.build.tasks.deployment.manifestutilities.manifest.outputmessages?view=msbuild-17-netcore). 1. If `Manifest.OutputMessages` contains any errors, fail signing. 1. Obtain the full path of the application manifest file from [`DeployManifest.EntryPoint`](https://learn.microsoft.com/dotnet/api/microsoft.build.tasks.deployment.manifestutilities.deploymanifest.entrypoint?view=msbuild-17-netcore)[`.ResolvedPath`](https://learn.microsoft.com/dotnet/api/microsoft.build.tasks.deployment.manifestutilities.basereference.resolvedpath?view=msbuild-17-netcore#microsoft-build-tasks-deployment-manifestutilities-basereference-resolvedpath). -1. If the application manifest file does not exist, log a warning and stop further processing of the deployment manifest file. +1. If the application manifest file does not exist, log an informational message and skip to step \#11. The application manifest and payload files will not be signed. 1. Read the application manifest file using `ManifestReader.ReadManifest(...)`. 1. Set `Manifest.ReadOnly` to `true` to ensure read-only mode. 1. Interate through both [`AssemblyReferences`](https://learn.microsoft.com/dotnet/api/microsoft.build.tasks.deployment.manifestutilities.manifest.assemblyreferences?view=msbuild-17-netcore) and [`FileReferences`](https://learn.microsoft.com/dotnet/api/microsoft.build.tasks.deployment.manifestutilities.manifest.filereferences?view=msbuild-17-netcore), manually resolve `TargetPath` property to the base path of the application manifest file. - * Note: it seems like calling `Manifest.ResolveFiles()` would resolve the full file path for every file dependency in the application manifest. However, this fails because `ResolveFiles()` assumes dependency files do not have `.deploy` file extension, but they do. We could temporarily remove the `.deploy` extension and then call `ResolveFiles()` but a user's glob patterns might filter out files based on the `.deploy` extension. The safest option is to resolve file paths ourselves. -1. Copy all files from the previous step, including the application manifest file itself, to a temporary directory. -1. Sign files in the following order: files alongside the application manifest, the application manifest itself, the deployment manifest, then `setup.exe`. -1. Copy the files back. -1. If the signed deployment manifest file is a `.vsto` file, copy it to the versioned application manifest file directory and overwrite if necessary. - +
+
+ Note: it seems like calling `Manifest.ResolveFiles(string[])` would resolve the full file path for every file dependency in the application manifest. However, this is unreliable because `ResolveFiles(string[])` assumes dependency files never have the `.deploy` file extension, but they might. We could temporarily remove the `.deploy` extension and then call `ResolveFiles(string[])`, but a user's glob patterns might filter out files based on the `.deploy` extension. The safest option is to resolve file paths ourselves. +1. Copy files from the previous steps to a temporary directory. +1. Sign files in the following order: payload files (if available), the application manifest (if available), the deployment manifest, then the bootstrapper. +1. Copy signed files back to their original locations. From 9548c6da30f8a4059d1d6b7cbe9c4304f6c98ba9 Mon Sep 17 00:00:00 2001 From: Damon Tivel Date: Sat, 7 Feb 2026 09:00:05 -0800 Subject: [PATCH 04/17] Update --- docs/specs/ClickOnce-Signing-Algorithm.md | 163 +++++++++++++++------- 1 file changed, 116 insertions(+), 47 deletions(-) diff --git a/docs/specs/ClickOnce-Signing-Algorithm.md b/docs/specs/ClickOnce-Signing-Algorithm.md index 3b3cad69..47302287 100644 --- a/docs/specs/ClickOnce-Signing-Algorithm.md +++ b/docs/specs/ClickOnce-Signing-Algorithm.md @@ -1,6 +1,6 @@ # ClickOnce Signing Algorithm -ClickOnce signing has been the source of numerous bugs, primarily because of fragile assumptions in Sign CLI's ClickOnce signing algorithm. +ClickOnce signing has been the source of numerous bugs, primarily because of fragile assumptions in Sign CLI's ClickOnce signing algorithm. This spec proposes algorithm changes that will fix those bugs while improving ClickOnce signing accuracy and predictability. ## Overview of a ClickOnce application @@ -9,64 +9,96 @@ A ClickOnce application consists of: * a deployment manifest: a ClickOnce `.application` or `.vsto` file. * an application manifest: a ClickOnce `.manifest` file, not to be confused with a [side-by-side or fusion manifest file](https://learn.microsoft.com/windows/win32/sbscs/application-manifests) with the same extension. * payload files: assemblies and other files required by the application. -* a bootstrapper: a `setup.exe` file for installing the ClickOnce application. +* a bootstrapper: a `setup.exe` file for installing the ClickOnce application, or a `Launcher.exe` file for activating and launching it via the ClickOnce runtime. -Publishing a ClickOnce application generates a bootstrapper, deployment and application manifest, and payload files. The application manifest and payload files are published to a versioned directory, and the deployment manifest is updated to point to the new application manifest. The bootstrapper points to the deployment manifest. +Publishing a ClickOnce application generates a bootstrapper, deployment and application manifest, and payload files. The application manifest and payload files are published to a versioned directory, and the deployment manifest is updated to point to the new application manifest. The bootstrapper points to the deployment manifest. ![ClickOnce file relationships](images/file-relationships.gif) ## Problem -Sign CLI's algorithm for signing ClickOnce applications is a source of bugs because of these fragile assumptions: +Sign CLI assumes: -- The directory containing the deployment manifest file contains a single ClickOnce application version. In reality, this directory can be the parent directory for many versions of the same ClickOnce application and/or the parent directory for many different ClickOnce applications. -- The directory containing the deployment manifest file contains at most one `.manifest` file in the directory tree. This assumption overlaps with the previous assumption, but even if the directory only contains a single ClickOnce application version, the application may contain multiple `.manifest` files (i.e.: an application manifest and one or more side-by-side manifests). +- The directory containing the deployment manifest file contains a single ClickOnce application version. In reality, this directory can be the parent directory for multiple versions of the same ClickOnce application and/or the parent directory for multiple ClickOnce applications. +- The directory containing the deployment manifest file contains at most one `.manifest` file in the directory tree. This assumption overlaps with the previous assumption, but even if the directory only contains a single ClickOnce application version, the application may contain multiple `.manifest` files (e.g., an application manifest and one or more side-by-side manifests). +- All `.manifest` files are ClickOnce application manifests, when in reality side-by-side (fusion) manifests also use the `.manifest` extension. +- All non-`.deploy` `.exe` files found anywhere in the deployment manifest's directory tree are treated as signable artifacts. The algorithm does not distinguish bootstrappers (`setup.exe`, `Launcher.exe`) from other `.exe` files, and it searches recursively rather than checking only the deployment manifest's directory. +- Manifests are well-formed and files referenced within them exist at the expected locations. When these assumptions fail, the algorithm's behavior is undefined or produces unclear error messages. +- Files are unique within the staging directory and won't be copied or processed multiple times due to directory tree traversal or references from multiple manifests. +- A single invocation should process an entire ClickOnce application, making it difficult to support partial re-signing scenarios where users want to re-sign only specific components. -The impact is that the algorithm is subject to over-copying, over-signing, failing to sign ClickOnce applications containing a side-by-side manifest, and difficulty batch signing multiple ClickOnce applications. +The impact is that the algorithm is subject to over-copying, over-signing, failing to sign ClickOnce applications containing a side-by-side manifest, and difficulty with batch signing multiple ClickOnce applications. There are two special cases that complicate signing: -1. VSTO publishing [signs the deployment manifest then copies it to the versioned application manifest file directory](https://devdiv.visualstudio.com/DevDiv/_git/VS?path=/src/ConfigData/BuildTargets/Microsoft.VisualStudio.Tools.Office.targets&version=GCba009548f0f1014f78b861e34bf4ef2700a28d25&line=473&lineEnd=483&lineStartColumn=9&lineEndColumn=11&lineStyle=plain&_a=contents), presumably for archival purposes. The current algorithm will discover each deployment manifest file and, in separate operations, attempt to sign each manifest and its dependencies. -1. A [comment](https://github.com/dotnet/sign/blob/e268c46059ae415749de057a14c8919c6f063049/src/Sign.Core/DataFormatSigners/ClickOnceSigner.cs#L88-L90) in the existing implementation says: - - ```C# - // It's possible that there might not actually be a .manifest file or any data files if the user just - // wants to re-sign an existing deployment manifest because e.g. the update URL has changed but nothing - // else has. In that case we don't need to touch the other files and we can just sign the deployment manifest. - ``` +1. VSTO publishing [signs the deployment manifest then copies it to the versioned application manifest file directory](https://devdiv.visualstudio.com/DevDiv/_git/VS?path=/src/ConfigData/BuildTargets/Microsoft.VisualStudio.Tools.Office.targets&version=GCa9fb919e0a7b3a62050cc77d5dc7dd7c38d50b0e&line=473&lineEnd=483&lineStartColumn=9&lineEndColumn=11&lineStyle=plain&_a=contents) for archival purposes. The current algorithm will discover each deployment manifest file and, in separate operations, attempt to sign each manifest and its dependencies. +1. Sometimes [manifests need to be re-signed](https://learn.microsoft.com/visualstudio/deployment/how-to-re-sign-application-and-deployment-manifests?view=vs-2022). For re-signing, users need to be able to disable implicit signing of related files. For example, a user should be able to re-sign only a deployment manifest or just deployment and application manifests without re-signing payload files. ## Proposed solution Given a deployment manifest file as a starting point, the algorithm will be updated to: -1. Resolve the local path for the application manifest using information in the deployment manifest. If the application manifest cannot be found, skip to step 3. In step 4, only the deployment manifest and bootstrapper will be signed. -1. Resolve the local path of payload files using information in the application manifest. -1. Resolve the local path of the bootstrapper in the same directory as the deployment manifest. -1. Copy and sign only these files in the order listed: - - payload files - - application manifest - - deployment manifest - - bootstrapper +1. Load the deployment manifest, locate the referenced application manifest, and refuse to continue if it is missing. +1. Stage only the files referenced by the manifests, sign the payloads first, then the application manifest, then the deployment manifest, and finally the bootstrapper. +1. After each signing stage, refresh manifest metadata so hashes, sizes, and entry-point information are consistent with the newly signed bits. + +Implementation specifics, including path resolution, `.deploy` renaming, and `ManifestUtilities` API calls, are detailed in Appendix B. The proposed solution will not attempt to mirror VSTO publishing and copy a signed deployment manifest into the application manifest directory. -To handle the special case of re-signing only the deployment manifest file, a user must either exclude other files (e.g.: using glob patterns) or isolate the deployment manifest file so that the application manifest file is not in the expected location relative to the deployment manifest file. +### File deduplication + +To prevent signing the same file multiple times when users specify overlapping inputs (e.g., both a deployment manifest and its dependencies via glob patterns), Sign CLI will track signed files using a `ConcurrentDictionary` in `SignOptions`. Using `ConcurrentDictionary` provides O(1) thread-safe lookups with minimal memory overhead (byte is the smallest value type), making it efficient for parallel signing of large file sets. + +This deduplication is scoped to a single CLI invocation. The dictionary lives in memory for the duration of one `sign` command and is not persisted to disk. Running the same command twice will re-sign all files on the second invocation. + +Before signing any file, signers will check if the file's canonical path has already been processed. This deduplication mechanism is independent of other algorithm changes and applies to all file types, not just ClickOnce files. + +For re-signing scenarios, two new options will be introduced (both require `--use-new-clickonce-signing`): + +* `--no-sign-clickonce-deps`: When specified, Sign CLI will update and sign only the explicitly specified manifest files without signing their dependencies (referenced manifests or payload files). Manifests are still updated before signing to refresh metadata. This allows users to re-sign only a deployment manifest, or only an application manifest, while ensuring the manifest's metadata remains consistent with its dependencies. +* `--no-update-clickonce-manifest`: When specified, Sign CLI will sign manifest files without calling `ResolveFiles()` and `UpdateFileInfo()`. This is useful when re-signing a manifest whose dependencies have not changed. + +These options are mutually exclusive (see [Option interactions](#option-interactions)). Without these options, Sign CLI will discover, update, and sign the complete ClickOnce application (deployment manifest, application manifest, and all referenced payload files). + +**Note**: Both `--no-sign-clickonce-deps` and `--no-update-clickonce-manifest` are only valid when used with `--use-new-clickonce-signing`. Attempting to use these options without enabling the new ClickOnce signing behavior will result in an error. + +### Rollout strategy + +The new ClickOnce signing algorithm is **opt-in**. Users must pass the `--use-new-clickonce-signing` flag to enable the proposed behavior. Without this flag, Sign CLI continues to use the current algorithm described in Appendix A. + +This approach avoids breaking existing workflows, particularly the deployment-manifest-only re-signing scenario, where the current algorithm succeeds when no application manifest is present, while the new algorithm requires one. It also gives users who depend on auto-discovery of inner deployment manifests (e.g., VSTO archival copies) or signing of non-`setup.exe` bootstrappers time to adapt their pipelines. + +The three new CLI options introduced by this spec are: + +| Option | Requires `--use-new-clickonce-signing` | Purpose | +|---|---|---| +| `--use-new-clickonce-signing` | No | Enables the manifest-driven signing algorithm described in Appendix B. | +| `--no-sign-clickonce-deps` | Yes | Updates and signs only the specified manifests; does not sign referenced payload files or dependent manifests. | +| `--no-update-clickonce-manifest` | Yes | Signs manifests without calling `ResolveFiles()` / `UpdateFileInfo()`. | + +When `--use-new-clickonce-signing` is omitted: + +* The current algorithm (Appendix A) is used. +* Passing `--no-sign-clickonce-deps` or `--no-update-clickonce-manifest` results in an error. + +In a future major release, the new algorithm may become the default and `--use-new-clickonce-signing` may be deprecated. That transition will be communicated through release notes and deprecation warnings in advance. ## Appendix A: Current algorithm In a temporary directory: -1. [[source](https://github.com/dotnet/sign/blob/e268c46059ae415749de057a14c8919c6f063049/src/Sign.Core/Signer.cs#L135-L147)] Copy the deployment manifest to a random file name with the same file extension (`.application` or `.vsto`). -1. [[source](https://github.com/dotnet/sign/blob/e268c46059ae415749de057a14c8919c6f063049/src/Sign.Core/DataFormatSigners/ClickOnceSigner.cs#L261-L274)] Copy all files from the deployment manifest's source directory and all its subdirectories to the temporary directory, while preserving the source's directory structure. _Because copying does not filter down to manifests and payload files, this step can result in overcopying._ -1. [[source](https://github.com/dotnet/sign/blob/e268c46059ae415749de057a14c8919c6f063049/src/Sign.Core/DataFormatSigners/ClickOnceSigner.cs#L97-L113)] Sign all `.deploy` and `.exe` files included by user's file matching patterns. _Previous overcopying can lead to oversigning in this step._ -1. [[source](https://github.com/dotnet/sign/blob/e268c46059ae415749de057a14c8919c6f063049/src/Sign.Core/DataFormatSigners/ClickOnceSigner.cs#L115-L123)] Remove the `.deploy` extension on any remaining files _excluded_ by file matching patterns. While these files may not be signed, they're still necessary to update the application manifest. -1. [[source](https://github.com/dotnet/sign/blob/e268c46059ae415749de057a14c8919c6f063049/src/Sign.Core/DataFormatSigners/ClickOnceSigner.cs#L130-L139)] Find files with the `.manifest` file extension. +1. [[source](https://github.com/dotnet/sign/blob/d4a580a9232e9d7aac931ea57b844e87e255af9a/src/Sign.Core/Signer.cs#L137-L149)] Copy the deployment manifest to a random file name with the same file extension (`.application` or `.vsto`). +1. [[source](https://github.com/dotnet/sign/blob/d4a580a9232e9d7aac931ea57b844e87e255af9a/src/Sign.Core/DataFormatSigners/ClickOnceSigner.cs#L261-L274)] Copy all files from the deployment manifest's source directory and all its subdirectories to the temporary directory, while preserving the source's directory structure. _Because copying does not filter down to manifests and payload files, this step can result in overcopying._ +1. [[source](https://github.com/dotnet/sign/blob/d4a580a9232e9d7aac931ea57b844e87e255af9a/src/Sign.Core/DataFormatSigners/ClickOnceSigner.cs#L97-L113)] Sign all `.deploy` and `.exe` files included by user's file matching patterns. _Previous overcopying can lead to oversigning in this step._ +1. [[source](https://github.com/dotnet/sign/blob/d4a580a9232e9d7aac931ea57b844e87e255af9a/src/Sign.Core/DataFormatSigners/ClickOnceSigner.cs#L115-L123)] Remove the `.deploy` extension on any remaining files _excluded_ by file matching patterns. While these files may not be signed, they're still necessary to update the application manifest. +1. [[source](https://github.com/dotnet/sign/blob/d4a580a9232e9d7aac931ea57b844e87e255af9a/src/Sign.Core/DataFormatSigners/ClickOnceSigner.cs#L130-L139)] Find files with the `.manifest` file extension. * If there are none, continue without signing application manifest. * If there is exactly one, assume it is the application manifest and sign it. * If there are multiple files, fail. _This can happen because of earlier overcopying or because side-by-side manifests are not ignored._ -1. [[source](https://github.com/dotnet/sign/blob/e268c46059ae415749de057a14c8919c6f063049/src/Sign.Core/DataFormatSigners/ClickOnceSigner.cs#L155-L183)] Sign all deployment manifests in file path length order descending. _Previous overcopying can lead to oversigning in this step._ -1. [[source](https://github.com/dotnet/sign/blob/e268c46059ae415749de057a14c8919c6f063049/src/Sign.Core/DataFormatSigners/ClickOnceSigner.cs#L155-L183)] Restore `.deploy` extensions. -1. [[source](https://github.com/dotnet/sign/blob/e268c46059ae415749de057a14c8919c6f063049/src/Sign.Core/DataFormatSigners/ClickOnceSigner.cs#L186-L189)] Copy files from the temporary directory back to the source location. _Previous overcopying can lead to overcopying in this step._ +1. [[source](https://github.com/dotnet/sign/blob/d4a580a9232e9d7aac931ea57b844e87e255af9a/src/Sign.Core/DataFormatSigners/ClickOnceSigner.cs#L155-L183)] Sign all deployment manifests in file path length order descending. _Previous overcopying can lead to oversigning in this step._ +1. [[source](https://github.com/dotnet/sign/blob/d4a580a9232e9d7aac931ea57b844e87e255af9a/src/Sign.Core/DataFormatSigners/ClickOnceSigner.cs#L185-L189)] Restore `.deploy` extensions. +1. [[source](https://github.com/dotnet/sign/blob/d4a580a9232e9d7aac931ea57b844e87e255af9a/src/Sign.Core/Signer.cs#L160-L161)] Copy files from the temporary directory back to the source location. _Previous overcopying can lead to overcopying in this step._ Here are two examples of how the current algorithm overcopies and oversigns. @@ -108,19 +140,56 @@ Here are two examples of how the current algorithm overcopies and oversigns. ## Appendix B: Proposed algorithm -1. If a file has a `.vsto` or `.application` file extension, read it as a deployment manifest using [`ManifestReader.ReadManifest(...)`](https://learn.microsoft.com/dotnet/api/microsoft.build.tasks.deployment.manifestutilities.manifestreader.readmanifest?view=msbuild-17-netcore#microsoft-build-tasks-deployment-manifestutilities-manifestreader-readmanifest(system-io-stream-system-boolean)). If file reading fails or the returned [`Manifest`](https://learn.microsoft.com/dotnet/api/microsoft.build.tasks.deployment.manifestutilities.manifest?view=msbuild-17-netcore) instance is not a [`DeployManifest`](https://learn.microsoft.com/dotnet/api/microsoft.build.tasks.deployment.manifestutilities.deploymanifest?view=msbuild-17-netcore), stop further processing; nothing will be signed. -1. Set [`Manifest.ReadOnly`](https://learn.microsoft.com/dotnet/api/microsoft.build.tasks.deployment.manifestutilities.manifest.readonly?view=msbuild-17-netcore) to `true` to ensure read-only mode. -1. Use [`Manifest.ResolveFiles(string[])`](https://learn.microsoft.com/dotnet/api/microsoft.build.tasks.deployment.manifestutilities.manifest.resolvefiles?view=msbuild-17-netcore#microsoft-build-tasks-deployment-manifestutilities-manifest-resolvefiles(system-string())) with the directory path of the deployment manifest file. If the application manifest cannot be located, the application manifest and payload files will be skipped for signing. -1. Log all messages in [`Manifest.OutputMessages`](https://learn.microsoft.com/dotnet/api/microsoft.build.tasks.deployment.manifestutilities.manifest.outputmessages?view=msbuild-17-netcore). -1. If `Manifest.OutputMessages` contains any errors, fail signing. -1. Obtain the full path of the application manifest file from [`DeployManifest.EntryPoint`](https://learn.microsoft.com/dotnet/api/microsoft.build.tasks.deployment.manifestutilities.deploymanifest.entrypoint?view=msbuild-17-netcore)[`.ResolvedPath`](https://learn.microsoft.com/dotnet/api/microsoft.build.tasks.deployment.manifestutilities.basereference.resolvedpath?view=msbuild-17-netcore#microsoft-build-tasks-deployment-manifestutilities-basereference-resolvedpath). -1. If the application manifest file does not exist, log an informational message and skip to step \#11. The application manifest and payload files will not be signed. -1. Read the application manifest file using `ManifestReader.ReadManifest(...)`. -1. Set `Manifest.ReadOnly` to `true` to ensure read-only mode. -1. Interate through both [`AssemblyReferences`](https://learn.microsoft.com/dotnet/api/microsoft.build.tasks.deployment.manifestutilities.manifest.assemblyreferences?view=msbuild-17-netcore) and [`FileReferences`](https://learn.microsoft.com/dotnet/api/microsoft.build.tasks.deployment.manifestutilities.manifest.filereferences?view=msbuild-17-netcore), manually resolve `TargetPath` property to the base path of the application manifest file. -
-
- Note: it seems like calling `Manifest.ResolveFiles(string[])` would resolve the full file path for every file dependency in the application manifest. However, this is unreliable because `ResolveFiles(string[])` assumes dependency files never have the `.deploy` file extension, but they might. We could temporarily remove the `.deploy` extension and then call `ResolveFiles(string[])`, but a user's glob patterns might filter out files based on the `.deploy` extension. The safest option is to resolve file paths ourselves. -1. Copy files from the previous steps to a temporary directory. -1. Sign files in the following order: payload files (if available), the application manifest (if available), the deployment manifest, then the bootstrapper. +### Default behavior (no options) + +1. Before processing any file, check if its canonical path (via `Path.GetFullPath()`) has already been signed by consulting the deduplication set in `SignOptions`. If already signed, skip the file. +1. Determine the file type and read the manifest: + - If a file has a `.vsto` or `.application` file extension, read it as a deployment manifest using [`ManifestReader.ReadManifest(...)`](https://learn.microsoft.com/dotnet/api/microsoft.build.tasks.deployment.manifestutilities.manifestreader.readmanifest?view=msbuild-17-netcore#microsoft-build-tasks-deployment-manifestutilities-manifestreader-readmanifest(system-io-stream-system-boolean)). If file reading fails or the returned [`Manifest`](https://learn.microsoft.com/dotnet/api/microsoft.build.tasks.deployment.manifestutilities.manifest?view=msbuild-17-netcore) instance is not a [`DeployManifest`](https://learn.microsoft.com/dotnet/api/microsoft.build.tasks.deployment.manifestutilities.deploymanifest?view=msbuild-17-netcore), stop further processing. The file will not be signed. + - If a file has a `.manifest` file extension, attempt to read it as an application manifest using `ManifestReader.ReadManifest(...)`. If file reading succeeds and the returned `Manifest` instance is an [`ApplicationManifest`](https://learn.microsoft.com/dotnet/api/microsoft.build.tasks.deployment.manifestutilities.applicationmanifest?view=msbuild-17-netcore), proceed with steps 8-11 below (skipping deployment manifest processing). If reading fails or the manifest is not an `ApplicationManifest`, stop further processing. The file will not be signed. +1. Ensure [`Manifest.ReadOnly`](https://learn.microsoft.com/dotnet/api/microsoft.build.tasks.deployment.manifestutilities.manifest.readonly?view=msbuild-17-netcore) is `false` so the manifest can be updated. +1. Call [`DeployManifest.ResolveFiles()`](https://learn.microsoft.com/dotnet/api/microsoft.build.tasks.deployment.manifestutilities.manifest.resolvefiles?view=msbuild-17-netcore#microsoft-build-tasks-deployment-manifestutilities-manifest-resolvefiles) to resolve file references relative to the deployment manifest's directory. Preserve the resolved relative paths (including `.deploy` suffixes) when staging files. +1. Log all messages in [`Manifest.OutputMessages`](https://learn.microsoft.com/dotnet/api/microsoft.build.tasks.deployment.manifestutilities.manifest.outputmessages?view=msbuild-17-netcore). If any are errors, log them and skip application manifest discovery (proceed to sign only the deployment manifest). `ResolveFiles()` may emit error-level messages for non-fatal conditions such as assembly metadata mismatches or optional references, so hard-failing would break real-world ClickOnce signing scenarios. +1. Obtain the full path of the application manifest file from [`DeployManifest.EntryPoint`](https://learn.microsoft.com/dotnet/api/microsoft.build.tasks.deployment.manifestutilities.deploymanifest.entrypoint?view=msbuild-17-netcore)[`.ResolvedPath`](https://learn.microsoft.com/dotnet/api/microsoft.build.tasks.deployment.manifestutilities.basereference.resolvedpath?view=msbuild-17-netcore#microsoft-build-tasks-deployment-manifestutilities-basereference-resolvedpath). If the path is empty or the file does not exist, fail signing with an error message that includes the expected path and suggests `--no-update-clickonce-manifest` +1. Read the application manifest file using `ManifestReader.ReadManifest(...)` and ensure `Manifest.ReadOnly` is `false`. +1. Call `ApplicationManifest.ResolveFiles()` to resolve file references, searching the application manifest's directory first, then the deployment manifest's directory as a fallback (if different). The fallback ensures that referenced files located at the deployment root, rather than alongside the application manifest, are resolved and staged correctly. Log all `OutputMessages`. If any are errors, log a warning and continue; `ResolveFiles()` may emit error-level messages for non-fatal conditions (e.g., assembly metadata mismatches, optional references). +1. Copy files referenced by `AssemblyReferences` and `FileReferences` to a temporary directory, preserving the original relative layout rooted at the application manifest directory. +1. Before signing begins, temporarily rename staged files whose names end with `.deploy` to their base names (for example, `MyApp.dll.deploy` → `MyApp.dll`). +1. Discover the bootstrapper by checking if a file named `setup.exe` or `Launcher.exe` exists in the same directory as the deployment manifest. If a bootstrapper exists in a different directory or has a different name, it should be signed separately using standard Authenticode signing outside of the ClickOnce signing algorithm. +1. Sign files in the following order: payload files (if available), the application manifest (if available), the deployment manifest (if available), then the bootstrapper (if available). Mark each file as signed in the deduplication set immediately after signing. +1. After payload files are signed, call `ApplicationManifest.UpdateFileInfo()` to refresh file hashes, sizes, and identities, then restore the `.deploy` suffixes. (`UpdateFileInfo()` hashes files at their `ResolvedPath`, which does not include `.deploy`; the suffixes must be absent when hashes are computed.) +1. After the application manifest is signed, call `DeployManifest.ResolveFiles()` to re-resolve file references, then call `DeployManifest.UpdateFileInfo()` to refresh the deployment manifest's metadata. When signing the deployment manifest with `mage.exe -update`, the `-appm` parameter updates the entry point reference to the application manifest. 1. Copy signed files back to their original locations. + +### With `--no-sign-clickonce-deps` + +When `--no-sign-clickonce-deps` is specified, Sign CLI will update and sign only the explicitly provided manifest files without signing their dependencies: + +1. Before processing any file, check the deduplication set. If already signed, skip the file. +1. For each file provided by the user: + - If the file has a `.vsto` or `.application` file extension, read it as a deployment manifest, call `DeployManifest.ResolveFiles()` and `DeployManifest.UpdateFileInfo()` to update its metadata based on the current state of referenced files, then sign only the deployment manifest. + - If the file has a `.manifest` file extension, read it as an application manifest, call `ApplicationManifest.ResolveFiles()` and `ApplicationManifest.UpdateFileInfo()` to update its metadata based on the current state of referenced files, then sign only the application manifest. + - For other file types, apply the standard signing logic. +1. Mark each signed file in the deduplication set. +1. Referenced manifests and payload files are discovered during the update process but are not signed. +1. The user is responsible for ensuring files are re-signed in the correct order (payload files first, then application manifest, then deployment manifest) if re-signing multiple manifests across separate invocations. + +### With `--no-update-clickonce-manifest` + +When `--no-update-clickonce-manifest` is specified, Sign CLI will sign manifest files without updating them: + +1. Before processing any file, check the deduplication set. If already signed, skip the file. +1. For each file provided by the user: + - If the file has a `.vsto` or `.application` file extension, read it as a deployment manifest and sign it without calling `DeployManifest.ResolveFiles()` or `DeployManifest.UpdateFileInfo()`. + - If the file has a `.manifest` file extension, read it as an application manifest and sign it without calling `ApplicationManifest.ResolveFiles()` or `ApplicationManifest.UpdateFileInfo()`. + - For other file types, apply the standard signing logic. +1. Mark each signed file in the deduplication set. +1. No discovery or metadata updates occur. +1. This option is useful when re-signing manifests whose dependencies have not changed. + +### Option interactions + +The `--no-sign-clickonce-deps` and `--no-update-clickonce-manifest` options are mutually exclusive: + +* `--no-sign-clickonce-deps` alone: Update and sign only specified manifests (dependencies discovered but not signed) +* `--no-update-clickonce-manifest` alone: Sign only specified manifests without updating them (no discovery of dependencies). This is the fastest option, but the user must ensure manifests are already consistent with their dependencies. +* Both options together: Not allowed. `--no-update-clickonce-manifest` skips all discovery and metadata updates, which fully subsumes the dependency-skipping behavior of `--no-sign-clickonce-deps`. Sign CLI will emit an error: `The '--no-sign-clickonce-deps' and '--no-update-clickonce-manifest' options cannot be combined.` From b54a6052b5d58180d1bdbaf3d05eb817b4cfb154 Mon Sep 17 00:00:00 2001 From: Damon Tivel Date: Tue, 17 Feb 2026 08:25:32 -0800 Subject: [PATCH 05/17] Add examples --- docs/specs/ClickOnce-Signing-Algorithm.md | 56 +++++++++++++++++++++++ 1 file changed, 56 insertions(+) diff --git a/docs/specs/ClickOnce-Signing-Algorithm.md b/docs/specs/ClickOnce-Signing-Algorithm.md index 47302287..81369908 100644 --- a/docs/specs/ClickOnce-Signing-Algorithm.md +++ b/docs/specs/ClickOnce-Signing-Algorithm.md @@ -84,6 +84,62 @@ When `--use-new-clickonce-signing` is omitted: In a future major release, the new algorithm may become the default and `--use-new-clickonce-signing` may be deprecated. That transition will be communicated through release notes and deprecation warnings in advance. +### CLI examples + +The examples below elide certificate and timestamp options (`-cfp`, `-cf`, `-p`, `-t`) for clarity. + +#### Sign a ClickOnce application (full pipeline) + +```shell +sign code certificate-store ... -co -b publish\ App.application +``` + +Signs the complete application: payload files, application manifest, deployment manifest, and bootstrapper, in the correct order. The algorithm follows the deployment manifest's references. Only the version it points to is signed. + +#### Sign a multi-version layout without over-signing + +Given a layout with multiple published versions: + +``` +publish\ +├── App.application ← points to v1.0.1.0 +└── Application Files\ + ├── App_1_0_0_0\... ← old version + └── App_1_0_1_0\... ← current version +``` + +```shell +# Current algorithm — crashes (SingleOrDefault with >1 .manifest file) +sign code certificate-store ... -b publish\ App.application + +# New algorithm — signs only v1.0.1.0 (the referenced version) +sign code certificate-store ... -co -b publish\ App.application +``` + +#### Sign multiple VSTO add-ins in one invocation + +```shell +sign code certificate-store ... -co -b Output\ **/*.vsto +``` + +Each `.vsto` file is processed independently: its referenced application manifest and payload DLL are discovered, signed, and deduplicated so that shared files are only signed once. + +#### Re-sign only a deployment manifest (after payload changes) + +```shell +sign code certificate-store ... -co --no-sign-clickonce-deps -b publish\ App.application +``` + +Updates the deployment manifest's metadata (sizes, hashes) to reflect the current state of its dependencies, then signs only the deployment manifest. Dependencies are not signed. + +#### Re-sign a manifest without updating metadata + +```shell +sign code certificate-store ... -co --no-update-clickonce-manifest -b publish\ App.application +``` + +Signs the deployment manifest as-is, without calling `ResolveFiles()` or `UpdateFileInfo()`. Useful when re-signing with a different certificate and dependencies have not changed. + ## Appendix A: Current algorithm In a temporary directory: From 598122d06efc9ba19dcd0b08cc23432ba592d646 Mon Sep 17 00:00:00 2001 From: Damon Tivel Date: Mon, 3 Aug 2026 13:32:20 -0700 Subject: [PATCH 06/17] Update ClickOnce signing algorithm spec Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 9ff869dc-e410-46dd-a9c8-8517357aa7cf --- docs/specs/ClickOnce-Signing-Algorithm.md | 147 ++++++++++++++-------- 1 file changed, 93 insertions(+), 54 deletions(-) diff --git a/docs/specs/ClickOnce-Signing-Algorithm.md b/docs/specs/ClickOnce-Signing-Algorithm.md index 81369908..a823861e 100644 --- a/docs/specs/ClickOnce-Signing-Algorithm.md +++ b/docs/specs/ClickOnce-Signing-Algorithm.md @@ -2,6 +2,8 @@ ClickOnce signing has been the source of numerous bugs, primarily because of fragile assumptions in Sign CLI's ClickOnce signing algorithm. This spec proposes algorithm changes that will fix those bugs while improving ClickOnce signing accuracy and predictability. +In this spec, ClickOnce signing includes both standard ClickOnce `.application` deployment manifests and VSTO `.vsto` deployment manifests. VSTO-specific behavior is identified where relevant. + ## Overview of a ClickOnce application A ClickOnce application consists of: @@ -9,9 +11,13 @@ A ClickOnce application consists of: * a deployment manifest: a ClickOnce `.application` or `.vsto` file. * an application manifest: a ClickOnce `.manifest` file, not to be confused with a [side-by-side or fusion manifest file](https://learn.microsoft.com/windows/win32/sbscs/application-manifests) with the same extension. * payload files: assemblies and other files required by the application. -* a bootstrapper: a `setup.exe` file for installing the ClickOnce application, or a `Launcher.exe` file for activating and launching it via the ClickOnce runtime. -Publishing a ClickOnce application generates a bootstrapper, deployment and application manifest, and payload files. The application manifest and payload files are published to a versioned directory, and the deployment manifest is updated to point to the new application manifest. The bootstrapper points to the deployment manifest. +Published output may also include: + +* optionally, a `setup.exe` bootstrapper for installing prerequisite packages before the ClickOnce application. +* optionally, a `Launcher.exe` file for launching the .NET application. `Launcher.exe` does not participate in ClickOnce activation. + +Publishing a ClickOnce application generates deployment and application manifests and payload files, and may also generate a bootstrapper or launcher. In a typical Visual Studio publish layout, the deployment manifest is in the parent publish directory, while the application manifest and payload files are in a version-specific child directory. The deployment manifest points to the application manifest for the current version. Other valid layouts may organize these files differently. ![ClickOnce file relationships](images/file-relationships.gif) @@ -22,9 +28,9 @@ Sign CLI assumes: - The directory containing the deployment manifest file contains a single ClickOnce application version. In reality, this directory can be the parent directory for multiple versions of the same ClickOnce application and/or the parent directory for multiple ClickOnce applications. - The directory containing the deployment manifest file contains at most one `.manifest` file in the directory tree. This assumption overlaps with the previous assumption, but even if the directory only contains a single ClickOnce application version, the application may contain multiple `.manifest` files (e.g., an application manifest and one or more side-by-side manifests). - All `.manifest` files are ClickOnce application manifests, when in reality side-by-side (fusion) manifests also use the `.manifest` extension. -- All non-`.deploy` `.exe` files found anywhere in the deployment manifest's directory tree are treated as signable artifacts. The algorithm does not distinguish bootstrappers (`setup.exe`, `Launcher.exe`) from other `.exe` files, and it searches recursively rather than checking only the deployment manifest's directory. +- All non-`.deploy` `.exe` files found anywhere in the deployment manifest's directory tree are treated as dependencies of that manifest. The defect is not that these executable files are Authenticode signed, but that the ClickOnce signer discovers them indiscriminately across all application versions instead of following the deployment manifest's references and checking only applicable adjacent files. - Manifests are well-formed and files referenced within them exist at the expected locations. When these assumptions fail, the algorithm's behavior is undefined or produces unclear error messages. -- Files are unique within the staging directory and won't be copied or processed multiple times due to directory tree traversal or references from multiple manifests. +- Files are unique within the staging directory and won't be copied or processed multiple times due to directory tree traversal or references from multiple manifests. The staging directory is the temporary working directory where Sign CLI copies files before signing and from which it copies successful results back to their original locations. - A single invocation should process an entire ClickOnce application, making it difficult to support partial re-signing scenarios where users want to re-sign only specific components. The impact is that the algorithm is subject to over-copying, over-signing, failing to sign ClickOnce applications containing a side-by-side manifest, and difficulty with batch signing multiple ClickOnce applications. @@ -38,51 +44,66 @@ There are two special cases that complicate signing: Given a deployment manifest file as a starting point, the algorithm will be updated to: -1. Load the deployment manifest, locate the referenced application manifest, and refuse to continue if it is missing. -1. Stage only the files referenced by the manifests, sign the payloads first, then the application manifest, then the deployment manifest, and finally the bootstrapper. -1. After each signing stage, refresh manifest metadata so hashes, sizes, and entry-point information are consistent with the newly signed bits. +1. Load the deployment manifest, locate the referenced application manifest, and, by default, refuse to continue if it is missing. +1. Stage only the files referenced by the manifests, sign the payloads first, then the application manifest, then the deployment manifest, and finally any applicable adjacent `setup.exe` or `Launcher.exe`. +1. After signing payloads and the application manifest, refresh the dependent manifest metadata so hashes, sizes, identities, and entry-point information are consistent with the newly signed files. Implementation specifics, including path resolution, `.deploy` renaming, and `ManifestUtilities` API calls, are detailed in Appendix B. -The proposed solution will not attempt to mirror VSTO publishing and copy a signed deployment manifest into the application manifest directory. +The proposed solution requires no changes to the .NET Framework `mage.exe` distributed with Sign CLI. The algorithm changes are implemented in Sign CLI by coordinating existing manifest APIs and signing behavior, and manifest signing continues through Sign CLI's existing programmatic manifest signer. Any new assembly or package reference needed to call `ManifestUtilities` is a Sign CLI implementation dependency, not a modification to `mage.exe`. + +The proposed solution will not attempt to mirror VSTO publishing and copy a signed deployment manifest into the application manifest directory. VSTO publishing creates this second copy for archival purposes, and [Microsoft's manifest re-signing guidance](https://learn.microsoft.com/visualstudio/deployment/how-to-re-sign-application-and-deployment-manifests?view=visualstudio) treats the copy as optional. No source reviewed for this spec establishes that ClickOnce or the VSTO runtime consumes the copy during installation, launch, update, or rollback. However, legacy or downstream tooling may expect the two files to remain identical. Users who require parity with the VSTO publish layout must explicitly copy the signed deployment manifest. Whether ClickOnce or VSTO publishing should stop producing the archival copy is outside the scope of Sign CLI. + +### Non-goals + +This proposal does not make a signing invocation transactional. Sign CLI may copy successfully signed files back before a later file fails, leaving a mix of signed and unsigned files. Invocation-wide atomicity is independent of the ClickOnce algorithm changes and should be addressed separately. -### File deduplication +### Signing operation coordination -To prevent signing the same file multiple times when users specify overlapping inputs (e.g., both a deployment manifest and its dependencies via glob patterns), Sign CLI will track signed files using a `ConcurrentDictionary` in `SignOptions`. Using `ConcurrentDictionary` provides O(1) thread-safe lookups with minimal memory overhead (byte is the smallest value type), making it efficient for parallel signing of large file sets. +To prevent signing the same file multiple times when users specify overlapping inputs (e.g., both a deployment manifest and its dependencies via glob patterns), Sign CLI will coordinate signing operations by canonical file path. The first caller to encounter a path owns its signing operation. Other callers that encounter the same path wait for that operation to complete and observe the same success or failure before continuing. A successful operation is complete only when its signed result is available for reuse by waiting callers, including callers that need the file in another staging layout. -This deduplication is scoped to a single CLI invocation. The dictionary lives in memory for the duration of one `sign` command and is not persisted to disk. Running the same command twice will re-sign all files on the second invocation. +This coordination is scoped to a single CLI invocation and is not persisted to disk. Running the same command twice will re-sign all files on the second invocation. -Before signing any file, signers will check if the file's canonical path has already been processed. This deduplication mechanism is independent of other algorithm changes and applies to all file types, not just ClickOnce files. +Coordinating the complete operation, rather than using a non-atomic check followed by marking the file as signed, prevents parallel inputs from signing the same file concurrently. Waiting for the owning operation also ensures that a manifest is not updated before a shared dependency has finished signing. The precise synchronization mechanism is an implementation detail. This mechanism is independent of other algorithm changes and applies to all file types, not just ClickOnce files. -For re-signing scenarios, two new options will be introduced (both require `--use-new-clickonce-signing`): +Implicit ClickOnce dependency traversal and user file matching remain separate. Starting from a deployment manifest, the ClickOnce signer follows only the referenced application version. Independently, Sign CLI continues to sign every signable file matched by the user's base directory and file patterns, including files in other version directories unless the user excludes them. Coordination prevents files reached through both paths from being signed twice. + +For re-signing scenarios, two new options will be introduced (both require ClickOnce signing algorithm version 2): * `--no-sign-clickonce-deps`: When specified, Sign CLI will update and sign only the explicitly specified manifest files without signing their dependencies (referenced manifests or payload files). Manifests are still updated before signing to refresh metadata. This allows users to re-sign only a deployment manifest, or only an application manifest, while ensuring the manifest's metadata remains consistent with its dependencies. * `--no-update-clickonce-manifest`: When specified, Sign CLI will sign manifest files without calling `ResolveFiles()` and `UpdateFileInfo()`. This is useful when re-signing a manifest whose dependencies have not changed. -These options are mutually exclusive (see [Option interactions](#option-interactions)). Without these options, Sign CLI will discover, update, and sign the complete ClickOnce application (deployment manifest, application manifest, and all referenced payload files). +These options are mutually exclusive (see [Option interactions](#option-interactions)). Without these options, Sign CLI will discover, update, and sign the complete ClickOnce application (deployment manifest, application manifest, all referenced payload files, and applicable adjacent executables). -**Note**: Both `--no-sign-clickonce-deps` and `--no-update-clickonce-manifest` are only valid when used with `--use-new-clickonce-signing`. Attempting to use these options without enabling the new ClickOnce signing behavior will result in an error. +**Note**: Both `--no-sign-clickonce-deps` and `--no-update-clickonce-manifest` are only valid when the effective ClickOnce signing algorithm version is 2. Attempting to use these options with version 1 will result in an error. ### Rollout strategy -The new ClickOnce signing algorithm is **opt-in**. Users must pass the `--use-new-clickonce-signing` flag to enable the proposed behavior. Without this flag, Sign CLI continues to use the current algorithm described in Appendix A. +The `--clickonce-signing-version ` option selects the ClickOnce signing algorithm. The supported values are: + +* `1`: The current algorithm described in Appendix A. +* `2`: The manifest-driven algorithm described in Appendix B. + +Initially, omitting `--clickonce-signing-version` selects version 1, so version 2 is opt-in. This avoids breaking existing workflows, particularly the deployment-manifest-only re-signing scenario, where version 1 succeeds when no application manifest is present while version 2's default behavior requires one. It also gives users who depend on recursive dependency discovery or VSTO archival-copy behavior time to adapt their pipelines. -This approach avoids breaking existing workflows, particularly the deployment-manifest-only re-signing scenario, where the current algorithm succeeds when no application manifest is present, while the new algorithm requires one. It also gives users who depend on auto-discovery of inner deployment manifests (e.g., VSTO archival copies) or signing of non-`setup.exe` bootstrappers time to adapt their pipelines. +The option affects only ClickOnce signing; other signing formats are unchanged. Values other than `1` or `2` result in a command-line validation error. The three new CLI options introduced by this spec are: -| Option | Requires `--use-new-clickonce-signing` | Purpose | +| Option | Requires version 2 | Purpose | |---|---|---| -| `--use-new-clickonce-signing` | No | Enables the manifest-driven signing algorithm described in Appendix B. | +| `--clickonce-signing-version ` | No | Selects ClickOnce signing algorithm version 1 or 2. Initially defaults to 1. | | `--no-sign-clickonce-deps` | Yes | Updates and signs only the specified manifests; does not sign referenced payload files or dependent manifests. | | `--no-update-clickonce-manifest` | Yes | Signs manifests without calling `ResolveFiles()` / `UpdateFileInfo()`. | -When `--use-new-clickonce-signing` is omitted: +No short alias is defined for `--clickonce-signing-version`. + +When the effective version is 1: * The current algorithm (Appendix A) is used. * Passing `--no-sign-clickonce-deps` or `--no-update-clickonce-manifest` results in an error. -In a future major release, the new algorithm may become the default and `--use-new-clickonce-signing` may be deprecated. That transition will be communicated through release notes and deprecation warnings in advance. +No warning will be emitted for version 1 during the initial version 2 opt-in period. After version 2 has proven stable, Sign CLI will begin warning when the effective version is 1 and recommend version 2. In a future major release, version 2 may become the default. Explicitly selecting version 1 will remain available as an escape hatch and will continue to emit the warning. Default changes and warnings will be communicated through release notes in advance. ### CLI examples @@ -91,10 +112,10 @@ The examples below elide certificate and timestamp options (`-cfp`, `-cf`, `-p`, #### Sign a ClickOnce application (full pipeline) ```shell -sign code certificate-store ... -co -b publish\ App.application +sign code certificate-store ... --clickonce-signing-version 2 -b publish\ App.application ``` -Signs the complete application: payload files, application manifest, deployment manifest, and bootstrapper, in the correct order. The algorithm follows the deployment manifest's references. Only the version it points to is signed. +Signs the complete application: payload files, application manifest, deployment manifest, and applicable adjacent executables, in the correct order. The algorithm follows the deployment manifest's references. Only the version it points to is implicitly discovered; other signable files matched by the user are still signed normally. #### Sign a multi-version layout without over-signing @@ -109,25 +130,25 @@ publish\ ``` ```shell -# Current algorithm — crashes (SingleOrDefault with >1 .manifest file) +# Version 1 — fails (SingleOrDefault with >1 .manifest file) sign code certificate-store ... -b publish\ App.application -# New algorithm — signs only v1.0.1.0 (the referenced version) -sign code certificate-store ... -co -b publish\ App.application +# Version 2 — implicitly discovers only v1.0.1.0 (the referenced version) +sign code certificate-store ... --clickonce-signing-version 2 -b publish\ App.application ``` #### Sign multiple VSTO add-ins in one invocation ```shell -sign code certificate-store ... -co -b Output\ **/*.vsto +sign code certificate-store ... --clickonce-signing-version 2 -b Output\ **/*.vsto ``` -Each `.vsto` file is processed independently: its referenced application manifest and payload DLL are discovered, signed, and deduplicated so that shared files are only signed once. +Each `.vsto` file is processed independently: its referenced application manifest and payload files are discovered and signed. Signing operations for shared files are coordinated so each file is signed only once and all dependents wait for signing to complete. #### Re-sign only a deployment manifest (after payload changes) ```shell -sign code certificate-store ... -co --no-sign-clickonce-deps -b publish\ App.application +sign code certificate-store ... --clickonce-signing-version 2 --no-sign-clickonce-deps -b publish\ App.application ``` Updates the deployment manifest's metadata (sizes, hashes) to reflect the current state of its dependencies, then signs only the deployment manifest. Dependencies are not signed. @@ -135,12 +156,12 @@ Updates the deployment manifest's metadata (sizes, hashes) to reflect the curren #### Re-sign a manifest without updating metadata ```shell -sign code certificate-store ... -co --no-update-clickonce-manifest -b publish\ App.application +sign code certificate-store ... --clickonce-signing-version 2 --no-update-clickonce-manifest -b publish\ App.application ``` Signs the deployment manifest as-is, without calling `ResolveFiles()` or `UpdateFileInfo()`. Useful when re-signing with a different certificate and dependencies have not changed. -## Appendix A: Current algorithm +## Appendix A: Signing algorithm version 1 In a temporary directory: @@ -194,38 +215,56 @@ Here are two examples of how the current algorithm overcopies and oversigns. myAddin.Excel.dll.manifest ``` -## Appendix B: Proposed algorithm +## Appendix B: Signing algorithm version 2 -### Default behavior (no options) +### Default behavior (no dependency options) -1. Before processing any file, check if its canonical path (via `Path.GetFullPath()`) has already been signed by consulting the deduplication set in `SignOptions`. If already signed, skip the file. +1. Before staging or signing any file, obtain the coordinated signing operation for its canonical source path (via `Path.GetFullPath()`). The first caller owns the operation; duplicate callers wait for its result before staging or consuming the file. 1. Determine the file type and read the manifest: - - If a file has a `.vsto` or `.application` file extension, read it as a deployment manifest using [`ManifestReader.ReadManifest(...)`](https://learn.microsoft.com/dotnet/api/microsoft.build.tasks.deployment.manifestutilities.manifestreader.readmanifest?view=msbuild-17-netcore#microsoft-build-tasks-deployment-manifestutilities-manifestreader-readmanifest(system-io-stream-system-boolean)). If file reading fails or the returned [`Manifest`](https://learn.microsoft.com/dotnet/api/microsoft.build.tasks.deployment.manifestutilities.manifest?view=msbuild-17-netcore) instance is not a [`DeployManifest`](https://learn.microsoft.com/dotnet/api/microsoft.build.tasks.deployment.manifestutilities.deploymanifest?view=msbuild-17-netcore), stop further processing. The file will not be signed. - - If a file has a `.manifest` file extension, attempt to read it as an application manifest using `ManifestReader.ReadManifest(...)`. If file reading succeeds and the returned `Manifest` instance is an [`ApplicationManifest`](https://learn.microsoft.com/dotnet/api/microsoft.build.tasks.deployment.manifestutilities.applicationmanifest?view=msbuild-17-netcore), proceed with steps 8-11 below (skipping deployment manifest processing). If reading fails or the manifest is not an `ApplicationManifest`, stop further processing. The file will not be signed. -1. Ensure [`Manifest.ReadOnly`](https://learn.microsoft.com/dotnet/api/microsoft.build.tasks.deployment.manifestutilities.manifest.readonly?view=msbuild-17-netcore) is `false` so the manifest can be updated. + - For a `.vsto` or `.application` file, follow [Deployment-manifest input](#deployment-manifest-input). + - For a `.manifest` file, follow [Standalone application-manifest input](#standalone-application-manifest-input). + - For any other file type, apply the standard signing logic. + +#### Deployment-manifest input + +1. Read the file using [`ManifestReader.ReadManifest(...)`](https://learn.microsoft.com/dotnet/api/microsoft.build.tasks.deployment.manifestutilities.manifestreader.readmanifest?view=msbuild-17-netcore#microsoft-build-tasks-deployment-manifestutilities-manifestreader-readmanifest(system-io-stream-system-boolean)). If reading fails or the returned [`Manifest`](https://learn.microsoft.com/dotnet/api/microsoft.build.tasks.deployment.manifestutilities.manifest?view=msbuild-17-netcore) is not a [`DeployManifest`](https://learn.microsoft.com/dotnet/api/microsoft.build.tasks.deployment.manifestutilities.deploymanifest?view=msbuild-17-netcore), fail the operation without signing the file. +1. Ensure [`Manifest.ReadOnly`](https://learn.microsoft.com/dotnet/api/microsoft.build.tasks.deployment.manifestutilities.manifest.readonly?view=msbuild-17-netcore) is `false` so the deployment manifest can be updated. 1. Call [`DeployManifest.ResolveFiles()`](https://learn.microsoft.com/dotnet/api/microsoft.build.tasks.deployment.manifestutilities.manifest.resolvefiles?view=msbuild-17-netcore#microsoft-build-tasks-deployment-manifestutilities-manifest-resolvefiles) to resolve file references relative to the deployment manifest's directory. Preserve the resolved relative paths (including `.deploy` suffixes) when staging files. -1. Log all messages in [`Manifest.OutputMessages`](https://learn.microsoft.com/dotnet/api/microsoft.build.tasks.deployment.manifestutilities.manifest.outputmessages?view=msbuild-17-netcore). If any are errors, log them and skip application manifest discovery (proceed to sign only the deployment manifest). `ResolveFiles()` may emit error-level messages for non-fatal conditions such as assembly metadata mismatches or optional references, so hard-failing would break real-world ClickOnce signing scenarios. -1. Obtain the full path of the application manifest file from [`DeployManifest.EntryPoint`](https://learn.microsoft.com/dotnet/api/microsoft.build.tasks.deployment.manifestutilities.deploymanifest.entrypoint?view=msbuild-17-netcore)[`.ResolvedPath`](https://learn.microsoft.com/dotnet/api/microsoft.build.tasks.deployment.manifestutilities.basereference.resolvedpath?view=msbuild-17-netcore#microsoft-build-tasks-deployment-manifestutilities-basereference-resolvedpath). If the path is empty or the file does not exist, fail signing with an error message that includes the expected path and suggests `--no-update-clickonce-manifest` -1. Read the application manifest file using `ManifestReader.ReadManifest(...)` and ensure `Manifest.ReadOnly` is `false`. -1. Call `ApplicationManifest.ResolveFiles()` to resolve file references, searching the application manifest's directory first, then the deployment manifest's directory as a fallback (if different). The fallback ensures that referenced files located at the deployment root, rather than alongside the application manifest, are resolved and staged correctly. Log all `OutputMessages`. If any are errors, log a warning and continue; `ResolveFiles()` may emit error-level messages for non-fatal conditions (e.g., assembly metadata mismatches, optional references). +1. Log all messages in [`Manifest.OutputMessages`](https://learn.microsoft.com/dotnet/api/microsoft.build.tasks.deployment.manifestutilities.manifest.outputmessages?view=msbuild-17-netcore). Continue when diagnostics do not prevent resolving required files; otherwise fail with a clear error. +1. Obtain the full path of the application manifest file from [`DeployManifest.EntryPoint`](https://learn.microsoft.com/dotnet/api/microsoft.build.tasks.deployment.manifestutilities.deploymanifest.entrypoint?view=msbuild-17-netcore)[`.ResolvedPath`](https://learn.microsoft.com/dotnet/api/microsoft.build.tasks.deployment.manifestutilities.basereference.resolvedpath?view=msbuild-17-netcore#microsoft-build-tasks-deployment-manifestutilities-basereference.resolvedpath). If the path is empty or the file does not exist, fail signing with an error message that identifies the expected path and suggests `--clickonce-signing-version 2 --no-update-clickonce-manifest` when the user only needs to re-sign the deployment manifest. +1. Read the application manifest file using `ManifestReader.ReadManifest(...)`. If reading fails or the returned manifest is not an [`ApplicationManifest`](https://learn.microsoft.com/dotnet/api/microsoft.build.tasks.deployment.manifestutilities.applicationmanifest?view=msbuild-17-netcore), fail with a clear error. Ensure `Manifest.ReadOnly` is `false`. +1. Call `ApplicationManifest.ResolveFiles()` to resolve file references, searching the application manifest's directory first, then the deployment manifest's directory as a fallback (if different). The fallback ensures that referenced files located at the deployment root, rather than alongside the application manifest, are resolved and staged correctly. Log all `OutputMessages`. Continue when diagnostics do not prevent resolving required files; otherwise fail with a clear error. 1. Copy files referenced by `AssemblyReferences` and `FileReferences` to a temporary directory, preserving the original relative layout rooted at the application manifest directory. 1. Before signing begins, temporarily rename staged files whose names end with `.deploy` to their base names (for example, `MyApp.dll.deploy` → `MyApp.dll`). -1. Discover the bootstrapper by checking if a file named `setup.exe` or `Launcher.exe` exists in the same directory as the deployment manifest. If a bootstrapper exists in a different directory or has a different name, it should be signed separately using standard Authenticode signing outside of the ClickOnce signing algorithm. -1. Sign files in the following order: payload files (if available), the application manifest (if available), the deployment manifest (if available), then the bootstrapper (if available). Mark each file as signed in the deduplication set immediately after signing. -1. After payload files are signed, call `ApplicationManifest.UpdateFileInfo()` to refresh file hashes, sizes, and identities, then restore the `.deploy` suffixes. (`UpdateFileInfo()` hashes files at their `ResolvedPath`, which does not include `.deploy`; the suffixes must be absent when hashes are computed.) -1. After the application manifest is signed, call `DeployManifest.ResolveFiles()` to re-resolve file references, then call `DeployManifest.UpdateFileInfo()` to refresh the deployment manifest's metadata. When signing the deployment manifest with `mage.exe -update`, the `-appm` parameter updates the entry point reference to the application manifest. -1. Copy signed files back to their original locations. +1. Discover applicable adjacent executables by checking if `setup.exe` or `Launcher.exe` exists in the same directory as the deployment manifest. `setup.exe` is an optional prerequisite bootstrapper; `Launcher.exe` launches the .NET application but does not participate in ClickOnce activation. A launcher or bootstrapper in a different directory or with a different name is not implicitly discovered as a dependency, but it will still be signed through standard Authenticode signing if matched by the user's file patterns. +1. Sign payload files. +1. Call `ApplicationManifest.UpdateFileInfo()` to refresh payload hashes, sizes, and identities, then restore the `.deploy` suffixes. (`UpdateFileInfo()` hashes files at their `ResolvedPath`, which does not include `.deploy`; the suffixes must be absent when hashes are computed.) Make the signed payload results available to waiting callers and complete their coordinated signing operations. Then sign the application manifest, make its signed result available, and complete its operation. +1. Signing the application manifest changes its signed bytes and can change its assembly identity, including its public key token. Call `DeployManifest.ResolveFiles()` to re-resolve file references, then call `DeployManifest.UpdateFileInfo()` to refresh the deployment manifest's hash, size, and entry-point identity before signing the deployment manifest. When signing the deployment manifest with `mage.exe -update`, the `-appm` parameter updates the entry point reference to the application manifest. Make the signed deployment manifest available and complete its operation. +1. Sign applicable adjacent executables, make their signed results available, and complete their operations. +1. Copy any remaining signed files back to their original locations and clean up the staging directory. + +#### Standalone application-manifest input + +1. Read the file using `ManifestReader.ReadManifest(...)`. If reading fails or the returned manifest is not an `ApplicationManifest`, fail the operation without signing the file. +1. Ensure `Manifest.ReadOnly` is `false`. +1. Call `ApplicationManifest.ResolveFiles()` relative to the application manifest's directory. Log all `OutputMessages`. Continue when diagnostics do not prevent resolving required files; otherwise fail with a clear error. +1. Copy files referenced by `AssemblyReferences` and `FileReferences` to a temporary directory, preserving their relative layout. +1. Temporarily rename staged files whose names end with `.deploy` to their base names. +1. Sign payload files. +1. Call `ApplicationManifest.UpdateFileInfo()`, restore the `.deploy` suffixes, make the signed payload results available, and complete their coordinated signing operations. +1. Sign the application manifest, make its signed result available, and complete its operation. +1. Copy any remaining signed files back to their original locations and clean up the staging directory. ### With `--no-sign-clickonce-deps` When `--no-sign-clickonce-deps` is specified, Sign CLI will update and sign only the explicitly provided manifest files without signing their dependencies: -1. Before processing any file, check the deduplication set. If already signed, skip the file. +1. Before processing any file, obtain or wait for its coordinated signing operation. 1. For each file provided by the user: - - If the file has a `.vsto` or `.application` file extension, read it as a deployment manifest, call `DeployManifest.ResolveFiles()` and `DeployManifest.UpdateFileInfo()` to update its metadata based on the current state of referenced files, then sign only the deployment manifest. - - If the file has a `.manifest` file extension, read it as an application manifest, call `ApplicationManifest.ResolveFiles()` and `ApplicationManifest.UpdateFileInfo()` to update its metadata based on the current state of referenced files, then sign only the application manifest. + - If the file has a `.vsto` or `.application` file extension, read it as a deployment manifest, call `DeployManifest.ResolveFiles()` and `DeployManifest.UpdateFileInfo()` to update its metadata based on the current state of the referenced application manifest, then sign only the deployment manifest. + - If the file has a `.manifest` file extension, read it as an application manifest and call `ApplicationManifest.ResolveFiles()`. Temporarily remove `.deploy` suffixes from referenced payloads, call `ApplicationManifest.UpdateFileInfo()`, restore the suffixes, then sign only the application manifest. - For other file types, apply the standard signing logic. -1. Mark each signed file in the deduplication set. +1. Complete each coordinated signing operation after its file is signed. 1. Referenced manifests and payload files are discovered during the update process but are not signed. 1. The user is responsible for ensuring files are re-signed in the correct order (payload files first, then application manifest, then deployment manifest) if re-signing multiple manifests across separate invocations. @@ -233,19 +272,19 @@ When `--no-sign-clickonce-deps` is specified, Sign CLI will update and sign only When `--no-update-clickonce-manifest` is specified, Sign CLI will sign manifest files without updating them: -1. Before processing any file, check the deduplication set. If already signed, skip the file. +1. Before processing any file, obtain or wait for its coordinated signing operation. 1. For each file provided by the user: - If the file has a `.vsto` or `.application` file extension, read it as a deployment manifest and sign it without calling `DeployManifest.ResolveFiles()` or `DeployManifest.UpdateFileInfo()`. - If the file has a `.manifest` file extension, read it as an application manifest and sign it without calling `ApplicationManifest.ResolveFiles()` or `ApplicationManifest.UpdateFileInfo()`. - For other file types, apply the standard signing logic. -1. Mark each signed file in the deduplication set. -1. No discovery or metadata updates occur. +1. Complete each coordinated signing operation after its file is signed. +1. No ClickOnce dependency discovery or manifest metadata updates occur. 1. This option is useful when re-signing manifests whose dependencies have not changed. ### Option interactions The `--no-sign-clickonce-deps` and `--no-update-clickonce-manifest` options are mutually exclusive: -* `--no-sign-clickonce-deps` alone: Update and sign only specified manifests (dependencies discovered but not signed) +* `--no-sign-clickonce-deps` alone: Update and sign only specified manifests (dependencies discovered but not signed). * `--no-update-clickonce-manifest` alone: Sign only specified manifests without updating them (no discovery of dependencies). This is the fastest option, but the user must ensure manifests are already consistent with their dependencies. * Both options together: Not allowed. `--no-update-clickonce-manifest` skips all discovery and metadata updates, which fully subsumes the dependency-skipping behavior of `--no-sign-clickonce-deps`. Sign CLI will emit an error: `The '--no-sign-clickonce-deps' and '--no-update-clickonce-manifest' options cannot be combined.` From aeca4a453654d778f1e745d02dbf03649732921a Mon Sep 17 00:00:00 2001 From: Damon Tivel Date: Wed, 5 Aug 2026 15:28:50 -0700 Subject: [PATCH 07/17] Clarify ClickOnce manifest hash selection Document the UpdateFileInfo overload used by algorithm version 2 and its SHA-256 compatibility behavior. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: e7d1383c-affb-4651-8d11-9bd70d5446c3 --- docs/specs/ClickOnce-Signing-Algorithm.md | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/docs/specs/ClickOnce-Signing-Algorithm.md b/docs/specs/ClickOnce-Signing-Algorithm.md index a823861e..4d0bb95e 100644 --- a/docs/specs/ClickOnce-Signing-Algorithm.md +++ b/docs/specs/ClickOnce-Signing-Algorithm.md @@ -71,7 +71,7 @@ Implicit ClickOnce dependency traversal and user file matching remain separate. For re-signing scenarios, two new options will be introduced (both require ClickOnce signing algorithm version 2): * `--no-sign-clickonce-deps`: When specified, Sign CLI will update and sign only the explicitly specified manifest files without signing their dependencies (referenced manifests or payload files). Manifests are still updated before signing to refresh metadata. This allows users to re-sign only a deployment manifest, or only an application manifest, while ensuring the manifest's metadata remains consistent with its dependencies. -* `--no-update-clickonce-manifest`: When specified, Sign CLI will sign manifest files without calling `ResolveFiles()` and `UpdateFileInfo()`. This is useful when re-signing a manifest whose dependencies have not changed. +* `--no-update-clickonce-manifest`: When specified, Sign CLI will sign manifest files without resolving files or updating file information. This is useful when re-signing a manifest whose dependencies have not changed. These options are mutually exclusive (see [Option interactions](#option-interactions)). Without these options, Sign CLI will discover, update, and sign the complete ClickOnce application (deployment manifest, application manifest, all referenced payload files, and applicable adjacent executables). @@ -94,7 +94,7 @@ The three new CLI options introduced by this spec are: |---|---|---| | `--clickonce-signing-version ` | No | Selects ClickOnce signing algorithm version 1 or 2. Initially defaults to 1. | | `--no-sign-clickonce-deps` | Yes | Updates and signs only the specified manifests; does not sign referenced payload files or dependent manifests. | -| `--no-update-clickonce-manifest` | Yes | Signs manifests without calling `ResolveFiles()` / `UpdateFileInfo()`. | +| `--no-update-clickonce-manifest` | Yes | Signs manifests without resolving files or updating file information. | No short alias is defined for `--clickonce-signing-version`. @@ -159,7 +159,7 @@ Updates the deployment manifest's metadata (sizes, hashes) to reflect the curren sign code certificate-store ... --clickonce-signing-version 2 --no-update-clickonce-manifest -b publish\ App.application ``` -Signs the deployment manifest as-is, without calling `ResolveFiles()` or `UpdateFileInfo()`. Useful when re-signing with a different certificate and dependencies have not changed. +Signs the deployment manifest as-is, without resolving files or updating file information. Useful when re-signing with a different certificate and dependencies have not changed. ## Appendix A: Signing algorithm version 1 @@ -217,6 +217,8 @@ Here are two examples of how the current algorithm overcopies and oversigns. ## Appendix B: Signing algorithm version 2 +`Manifest` provides a parameterless `UpdateFileInfo()` overload and an `UpdateFileInfo(string targetFrameworkVersion)` overload. Version 2 does not call the parameterless overload, which computes SHA-1 hashes for referenced files. Whenever version 2 updates file information, it calls `UpdateFileInfo("v4.5")`. MSBuild's manifest utility implementation selects SHA-256 when the supplied target framework version is greater than `"v4.0"`; `"v4.5"` selects the hashing behavior and does not represent the application's target framework. This preserves Sign CLI's current SHA-256 behavior and does not add support for ClickOnce runtimes that require SHA-1 manifest hashes. + ### Default behavior (no dependency options) 1. Before staging or signing any file, obtain the coordinated signing operation for its canonical source path (via `Path.GetFullPath()`). The first caller owns the operation; duplicate callers wait for its result before staging or consuming the file. @@ -238,8 +240,8 @@ Here are two examples of how the current algorithm overcopies and oversigns. 1. Before signing begins, temporarily rename staged files whose names end with `.deploy` to their base names (for example, `MyApp.dll.deploy` → `MyApp.dll`). 1. Discover applicable adjacent executables by checking if `setup.exe` or `Launcher.exe` exists in the same directory as the deployment manifest. `setup.exe` is an optional prerequisite bootstrapper; `Launcher.exe` launches the .NET application but does not participate in ClickOnce activation. A launcher or bootstrapper in a different directory or with a different name is not implicitly discovered as a dependency, but it will still be signed through standard Authenticode signing if matched by the user's file patterns. 1. Sign payload files. -1. Call `ApplicationManifest.UpdateFileInfo()` to refresh payload hashes, sizes, and identities, then restore the `.deploy` suffixes. (`UpdateFileInfo()` hashes files at their `ResolvedPath`, which does not include `.deploy`; the suffixes must be absent when hashes are computed.) Make the signed payload results available to waiting callers and complete their coordinated signing operations. Then sign the application manifest, make its signed result available, and complete its operation. -1. Signing the application manifest changes its signed bytes and can change its assembly identity, including its public key token. Call `DeployManifest.ResolveFiles()` to re-resolve file references, then call `DeployManifest.UpdateFileInfo()` to refresh the deployment manifest's hash, size, and entry-point identity before signing the deployment manifest. When signing the deployment manifest with `mage.exe -update`, the `-appm` parameter updates the entry point reference to the application manifest. Make the signed deployment manifest available and complete its operation. +1. Call `ApplicationManifest.UpdateFileInfo("v4.5")` to refresh payload SHA-256 hashes, sizes, and identities, then restore the `.deploy` suffixes. (`UpdateFileInfo(string)` hashes files at their `ResolvedPath`, which does not include `.deploy`; the suffixes must be absent when hashes are computed.) Make the signed payload results available to waiting callers and complete their coordinated signing operations. Then sign the application manifest, make its signed result available, and complete its operation. +1. Signing the application manifest changes its signed bytes and can change its assembly identity, including its public key token. Call `DeployManifest.ResolveFiles()` to re-resolve file references, then call `DeployManifest.UpdateFileInfo("v4.5")` to refresh the SHA-256 hash and size of the entry-point application-manifest reference. Ensure that the entry-point identity matches the signed application manifest before signing the deployment manifest. When signing the deployment manifest with `mage.exe -update`, the `-appm` parameter performs this entry-point update. Make the signed deployment manifest available and complete its operation. 1. Sign applicable adjacent executables, make their signed results available, and complete their operations. 1. Copy any remaining signed files back to their original locations and clean up the staging directory. @@ -251,7 +253,7 @@ Here are two examples of how the current algorithm overcopies and oversigns. 1. Copy files referenced by `AssemblyReferences` and `FileReferences` to a temporary directory, preserving their relative layout. 1. Temporarily rename staged files whose names end with `.deploy` to their base names. 1. Sign payload files. -1. Call `ApplicationManifest.UpdateFileInfo()`, restore the `.deploy` suffixes, make the signed payload results available, and complete their coordinated signing operations. +1. Call `ApplicationManifest.UpdateFileInfo("v4.5")`, restore the `.deploy` suffixes, make the signed payload results available, and complete their coordinated signing operations. 1. Sign the application manifest, make its signed result available, and complete its operation. 1. Copy any remaining signed files back to their original locations and clean up the staging directory. @@ -261,8 +263,8 @@ When `--no-sign-clickonce-deps` is specified, Sign CLI will update and sign only 1. Before processing any file, obtain or wait for its coordinated signing operation. 1. For each file provided by the user: - - If the file has a `.vsto` or `.application` file extension, read it as a deployment manifest, call `DeployManifest.ResolveFiles()` and `DeployManifest.UpdateFileInfo()` to update its metadata based on the current state of the referenced application manifest, then sign only the deployment manifest. - - If the file has a `.manifest` file extension, read it as an application manifest and call `ApplicationManifest.ResolveFiles()`. Temporarily remove `.deploy` suffixes from referenced payloads, call `ApplicationManifest.UpdateFileInfo()`, restore the suffixes, then sign only the application manifest. + - If the file has a `.vsto` or `.application` file extension, read it as a deployment manifest, call `DeployManifest.ResolveFiles()` and `DeployManifest.UpdateFileInfo("v4.5")` to update its metadata based on the current state of the referenced application manifest, then sign only the deployment manifest. + - If the file has a `.manifest` file extension, read it as an application manifest and call `ApplicationManifest.ResolveFiles()`. Temporarily remove `.deploy` suffixes from referenced payloads, call `ApplicationManifest.UpdateFileInfo("v4.5")`, restore the suffixes, then sign only the application manifest. - For other file types, apply the standard signing logic. 1. Complete each coordinated signing operation after its file is signed. 1. Referenced manifests and payload files are discovered during the update process but are not signed. @@ -274,8 +276,8 @@ When `--no-update-clickonce-manifest` is specified, Sign CLI will sign manifest 1. Before processing any file, obtain or wait for its coordinated signing operation. 1. For each file provided by the user: - - If the file has a `.vsto` or `.application` file extension, read it as a deployment manifest and sign it without calling `DeployManifest.ResolveFiles()` or `DeployManifest.UpdateFileInfo()`. - - If the file has a `.manifest` file extension, read it as an application manifest and sign it without calling `ApplicationManifest.ResolveFiles()` or `ApplicationManifest.UpdateFileInfo()`. + - If the file has a `.vsto` or `.application` file extension, read it as a deployment manifest and sign it without resolving files or updating file information. + - If the file has a `.manifest` file extension, read it as an application manifest and sign it without resolving files or updating file information. - For other file types, apply the standard signing logic. 1. Complete each coordinated signing operation after its file is signed. 1. No ClickOnce dependency discovery or manifest metadata updates occur. From 5565413be3344d452c3882bb80f37fe6110bd1a1 Mon Sep 17 00:00:00 2001 From: Damon Tivel Date: Wed, 5 Aug 2026 16:01:51 -0700 Subject: [PATCH 08/17] Clarify staged ClickOnce path resolution Require manifest metadata updates to use staged references while leaving the path-establishment mechanism implementation-defined. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: e7d1383c-affb-4651-8d11-9bd70d5446c3 --- docs/specs/ClickOnce-Signing-Algorithm.md | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/docs/specs/ClickOnce-Signing-Algorithm.md b/docs/specs/ClickOnce-Signing-Algorithm.md index 4d0bb95e..541cf7ab 100644 --- a/docs/specs/ClickOnce-Signing-Algorithm.md +++ b/docs/specs/ClickOnce-Signing-Algorithm.md @@ -45,8 +45,8 @@ There are two special cases that complicate signing: Given a deployment manifest file as a starting point, the algorithm will be updated to: 1. Load the deployment manifest, locate the referenced application manifest, and, by default, refuse to continue if it is missing. -1. Stage only the files referenced by the manifests, sign the payloads first, then the application manifest, then the deployment manifest, and finally any applicable adjacent `setup.exe` or `Launcher.exe`. -1. After signing payloads and the application manifest, refresh the dependent manifest metadata so hashes, sizes, identities, and entry-point information are consistent with the newly signed files. +1. Stage only the files referenced by the manifests and any applicable adjacent `setup.exe` or `Launcher.exe`. +1. Sign the payloads, refresh the application manifest's metadata, and sign the application manifest. Then refresh the deployment manifest's entry-point metadata, sign the deployment manifest, and finally sign the adjacent executables so hashes, sizes, identities, and entry-point information are consistent with the newly signed files. Implementation specifics, including path resolution, `.deploy` renaming, and `ManifestUtilities` API calls, are detailed in Appendix B. @@ -236,12 +236,13 @@ Here are two examples of how the current algorithm overcopies and oversigns. 1. Obtain the full path of the application manifest file from [`DeployManifest.EntryPoint`](https://learn.microsoft.com/dotnet/api/microsoft.build.tasks.deployment.manifestutilities.deploymanifest.entrypoint?view=msbuild-17-netcore)[`.ResolvedPath`](https://learn.microsoft.com/dotnet/api/microsoft.build.tasks.deployment.manifestutilities.basereference.resolvedpath?view=msbuild-17-netcore#microsoft-build-tasks-deployment-manifestutilities-basereference.resolvedpath). If the path is empty or the file does not exist, fail signing with an error message that identifies the expected path and suggests `--clickonce-signing-version 2 --no-update-clickonce-manifest` when the user only needs to re-sign the deployment manifest. 1. Read the application manifest file using `ManifestReader.ReadManifest(...)`. If reading fails or the returned manifest is not an [`ApplicationManifest`](https://learn.microsoft.com/dotnet/api/microsoft.build.tasks.deployment.manifestutilities.applicationmanifest?view=msbuild-17-netcore), fail with a clear error. Ensure `Manifest.ReadOnly` is `false`. 1. Call `ApplicationManifest.ResolveFiles()` to resolve file references, searching the application manifest's directory first, then the deployment manifest's directory as a fallback (if different). The fallback ensures that referenced files located at the deployment root, rather than alongside the application manifest, are resolved and staged correctly. Log all `OutputMessages`. Continue when diagnostics do not prevent resolving required files; otherwise fail with a clear error. -1. Copy files referenced by `AssemblyReferences` and `FileReferences` to a temporary directory, preserving the original relative layout rooted at the application manifest directory. +1. Copy the deployment manifest, application manifest, and files referenced by the application manifest's `AssemblyReferences` and `FileReferences` to a temporary directory, preserving their relative layout. 1. Before signing begins, temporarily rename staged files whose names end with `.deploy` to their base names (for example, `MyApp.dll.deploy` → `MyApp.dll`). +1. Before signing or updating manifest metadata, ensure that the application manifest's references resolve to the corresponding staged files after any `.deploy` suffixes have been removed. No `ResolvedPath` used by `UpdateFileInfo(...)` may identify a source file outside the staging directory. How the implementation establishes the staged paths is an implementation detail. Fail if any required staged file cannot be resolved. 1. Discover applicable adjacent executables by checking if `setup.exe` or `Launcher.exe` exists in the same directory as the deployment manifest. `setup.exe` is an optional prerequisite bootstrapper; `Launcher.exe` launches the .NET application but does not participate in ClickOnce activation. A launcher or bootstrapper in a different directory or with a different name is not implicitly discovered as a dependency, but it will still be signed through standard Authenticode signing if matched by the user's file patterns. 1. Sign payload files. 1. Call `ApplicationManifest.UpdateFileInfo("v4.5")` to refresh payload SHA-256 hashes, sizes, and identities, then restore the `.deploy` suffixes. (`UpdateFileInfo(string)` hashes files at their `ResolvedPath`, which does not include `.deploy`; the suffixes must be absent when hashes are computed.) Make the signed payload results available to waiting callers and complete their coordinated signing operations. Then sign the application manifest, make its signed result available, and complete its operation. -1. Signing the application manifest changes its signed bytes and can change its assembly identity, including its public key token. Call `DeployManifest.ResolveFiles()` to re-resolve file references, then call `DeployManifest.UpdateFileInfo("v4.5")` to refresh the SHA-256 hash and size of the entry-point application-manifest reference. Ensure that the entry-point identity matches the signed application manifest before signing the deployment manifest. When signing the deployment manifest with `mage.exe -update`, the `-appm` parameter performs this entry-point update. Make the signed deployment manifest available and complete its operation. +1. Signing the application manifest changes its signed bytes and can change its assembly identity, including its public key token. Ensure that the deployment manifest's entry-point `ResolvedPath` identifies the signed staged application manifest, then call `DeployManifest.UpdateFileInfo("v4.5")` to refresh the entry-point reference's SHA-256 hash and size. Ensure that the entry-point identity matches the signed application manifest before signing the deployment manifest. When signing the deployment manifest with `mage.exe -update`, the `-appm` parameter performs this entry-point update. Make the signed deployment manifest available and complete its operation. 1. Sign applicable adjacent executables, make their signed results available, and complete their operations. 1. Copy any remaining signed files back to their original locations and clean up the staging directory. @@ -250,8 +251,9 @@ Here are two examples of how the current algorithm overcopies and oversigns. 1. Read the file using `ManifestReader.ReadManifest(...)`. If reading fails or the returned manifest is not an `ApplicationManifest`, fail the operation without signing the file. 1. Ensure `Manifest.ReadOnly` is `false`. 1. Call `ApplicationManifest.ResolveFiles()` relative to the application manifest's directory. Log all `OutputMessages`. Continue when diagnostics do not prevent resolving required files; otherwise fail with a clear error. -1. Copy files referenced by `AssemblyReferences` and `FileReferences` to a temporary directory, preserving their relative layout. +1. Copy the application manifest and files referenced by its `AssemblyReferences` and `FileReferences` to a temporary directory, preserving their relative layout. 1. Temporarily rename staged files whose names end with `.deploy` to their base names. +1. Before signing or updating manifest metadata, ensure that the application manifest's references resolve to the corresponding staged files after any `.deploy` suffixes have been removed. No `ResolvedPath` used by `UpdateFileInfo(...)` may identify a source file outside the staging directory. How the implementation establishes the staged paths is an implementation detail. Fail if any required staged file cannot be resolved. 1. Sign payload files. 1. Call `ApplicationManifest.UpdateFileInfo("v4.5")`, restore the `.deploy` suffixes, make the signed payload results available, and complete their coordinated signing operations. 1. Sign the application manifest, make its signed result available, and complete its operation. From bbda9a55a7eca313d4d80cda8cfe811c79838b42 Mon Sep 17 00:00:00 2001 From: Damon Tivel Date: Wed, 5 Aug 2026 16:16:25 -0700 Subject: [PATCH 09/17] Clarify explicit ClickOnce manifest ordering Require application manifest signing to complete before updating and signing an explicitly provided deployment manifest. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: e7d1383c-affb-4651-8d11-9bd70d5446c3 --- docs/specs/ClickOnce-Signing-Algorithm.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/specs/ClickOnce-Signing-Algorithm.md b/docs/specs/ClickOnce-Signing-Algorithm.md index 541cf7ab..f207cc9e 100644 --- a/docs/specs/ClickOnce-Signing-Algorithm.md +++ b/docs/specs/ClickOnce-Signing-Algorithm.md @@ -264,6 +264,7 @@ Here are two examples of how the current algorithm overcopies and oversigns. When `--no-sign-clickonce-deps` is specified, Sign CLI will update and sign only the explicitly provided manifest files without signing their dependencies: 1. Before processing any file, obtain or wait for its coordinated signing operation. +1. If both a deployment manifest and its referenced application manifest are explicitly provided in the same invocation, update and sign the application manifest first, regardless of input order or parallel scheduling. The deployment-manifest operation must wait for the application-manifest operation to complete successfully before refreshing its entry-point metadata and signing. If the application-manifest operation fails, do not sign the deployment manifest. 1. For each file provided by the user: - If the file has a `.vsto` or `.application` file extension, read it as a deployment manifest, call `DeployManifest.ResolveFiles()` and `DeployManifest.UpdateFileInfo("v4.5")` to update its metadata based on the current state of the referenced application manifest, then sign only the deployment manifest. - If the file has a `.manifest` file extension, read it as an application manifest and call `ApplicationManifest.ResolveFiles()`. Temporarily remove `.deploy` suffixes from referenced payloads, call `ApplicationManifest.UpdateFileInfo("v4.5")`, restore the suffixes, then sign only the application manifest. From 92ac9ca2437c2044b198d8823c72a8d9d093aaae Mon Sep 17 00:00:00 2001 From: Damon Tivel Date: Wed, 5 Aug 2026 16:32:29 -0700 Subject: [PATCH 10/17] Clarify deployment entry-point identity refresh Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: e7d1383c-affb-4651-8d11-9bd70d5446c3 --- docs/specs/ClickOnce-Signing-Algorithm.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/specs/ClickOnce-Signing-Algorithm.md b/docs/specs/ClickOnce-Signing-Algorithm.md index f207cc9e..9a495c17 100644 --- a/docs/specs/ClickOnce-Signing-Algorithm.md +++ b/docs/specs/ClickOnce-Signing-Algorithm.md @@ -266,7 +266,7 @@ When `--no-sign-clickonce-deps` is specified, Sign CLI will update and sign only 1. Before processing any file, obtain or wait for its coordinated signing operation. 1. If both a deployment manifest and its referenced application manifest are explicitly provided in the same invocation, update and sign the application manifest first, regardless of input order or parallel scheduling. The deployment-manifest operation must wait for the application-manifest operation to complete successfully before refreshing its entry-point metadata and signing. If the application-manifest operation fails, do not sign the deployment manifest. 1. For each file provided by the user: - - If the file has a `.vsto` or `.application` file extension, read it as a deployment manifest, call `DeployManifest.ResolveFiles()` and `DeployManifest.UpdateFileInfo("v4.5")` to update its metadata based on the current state of the referenced application manifest, then sign only the deployment manifest. + - If the file has a `.vsto` or `.application` file extension, read it as a deployment manifest and call `DeployManifest.ResolveFiles()`. Call `DeployManifest.UpdateFileInfo("v4.5")` to refresh the entry-point reference's SHA-256 hash and size, ensure that the entry-point identity matches the referenced application manifest's current identity, then sign only the deployment manifest. - If the file has a `.manifest` file extension, read it as an application manifest and call `ApplicationManifest.ResolveFiles()`. Temporarily remove `.deploy` suffixes from referenced payloads, call `ApplicationManifest.UpdateFileInfo("v4.5")`, restore the suffixes, then sign only the application manifest. - For other file types, apply the standard signing logic. 1. Complete each coordinated signing operation after its file is signed. From cde542e6024752446d94ced0f0aca6706411ca59 Mon Sep 17 00:00:00 2001 From: Damon Tivel Date: Wed, 5 Aug 2026 17:45:07 -0700 Subject: [PATCH 11/17] Clarify ClickOnce deploy file mapping Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: e7d1383c-affb-4651-8d11-9bd70d5446c3 --- docs/specs/ClickOnce-Signing-Algorithm.md | 28 ++++++++++++----------- 1 file changed, 15 insertions(+), 13 deletions(-) diff --git a/docs/specs/ClickOnce-Signing-Algorithm.md b/docs/specs/ClickOnce-Signing-Algorithm.md index 9a495c17..b992a471 100644 --- a/docs/specs/ClickOnce-Signing-Algorithm.md +++ b/docs/specs/ClickOnce-Signing-Algorithm.md @@ -219,6 +219,8 @@ Here are two examples of how the current algorithm overcopies and oversigns. `Manifest` provides a parameterless `UpdateFileInfo()` overload and an `UpdateFileInfo(string targetFrameworkVersion)` overload. Version 2 does not call the parameterless overload, which computes SHA-1 hashes for referenced files. Whenever version 2 updates file information, it calls `UpdateFileInfo("v4.5")`. MSBuild's manifest utility implementation selects SHA-256 when the supplied target framework version is greater than `"v4.0"`; `"v4.5"` selects the hashing behavior and does not represent the application's target framework. This preserves Sign CLI's current SHA-256 behavior and does not add support for ClickOnce runtimes that require SHA-1 manifest hashes. +When `DeployManifest.MapFileExtensions` is `true`, ClickOnce file-extension mapping leaves manifest target paths unchanged and appends one additional `.deploy` suffix to physical published payload files. Whenever version 2 stages a mapped payload, it preserves the physical filename, records that the additional suffix was mapped, and copies the source without renaming it. Before resolving staged references or calling `UpdateFileInfo(...)`, temporarily remove only the recorded suffix from the staged copy, then restore it afterward. Do not remove a `.deploy` suffix that is part of the manifest target path. + ### Default behavior (no dependency options) 1. Before staging or signing any file, obtain the coordinated signing operation for its canonical source path (via `Path.GetFullPath()`). The first caller owns the operation; duplicate callers wait for its result before staging or consuming the file. @@ -231,17 +233,17 @@ Here are two examples of how the current algorithm overcopies and oversigns. 1. Read the file using [`ManifestReader.ReadManifest(...)`](https://learn.microsoft.com/dotnet/api/microsoft.build.tasks.deployment.manifestutilities.manifestreader.readmanifest?view=msbuild-17-netcore#microsoft-build-tasks-deployment-manifestutilities-manifestreader-readmanifest(system-io-stream-system-boolean)). If reading fails or the returned [`Manifest`](https://learn.microsoft.com/dotnet/api/microsoft.build.tasks.deployment.manifestutilities.manifest?view=msbuild-17-netcore) is not a [`DeployManifest`](https://learn.microsoft.com/dotnet/api/microsoft.build.tasks.deployment.manifestutilities.deploymanifest?view=msbuild-17-netcore), fail the operation without signing the file. 1. Ensure [`Manifest.ReadOnly`](https://learn.microsoft.com/dotnet/api/microsoft.build.tasks.deployment.manifestutilities.manifest.readonly?view=msbuild-17-netcore) is `false` so the deployment manifest can be updated. -1. Call [`DeployManifest.ResolveFiles()`](https://learn.microsoft.com/dotnet/api/microsoft.build.tasks.deployment.manifestutilities.manifest.resolvefiles?view=msbuild-17-netcore#microsoft-build-tasks-deployment-manifestutilities-manifest-resolvefiles) to resolve file references relative to the deployment manifest's directory. Preserve the resolved relative paths (including `.deploy` suffixes) when staging files. +1. Call [`DeployManifest.ResolveFiles(string[])`](https://learn.microsoft.com/dotnet/api/microsoft.build.tasks.deployment.manifestutilities.manifest.resolvefiles?view=msbuild-17-netcore) with the deployment manifest's directory to resolve its references, including the application-manifest entry point. 1. Log all messages in [`Manifest.OutputMessages`](https://learn.microsoft.com/dotnet/api/microsoft.build.tasks.deployment.manifestutilities.manifest.outputmessages?view=msbuild-17-netcore). Continue when diagnostics do not prevent resolving required files; otherwise fail with a clear error. 1. Obtain the full path of the application manifest file from [`DeployManifest.EntryPoint`](https://learn.microsoft.com/dotnet/api/microsoft.build.tasks.deployment.manifestutilities.deploymanifest.entrypoint?view=msbuild-17-netcore)[`.ResolvedPath`](https://learn.microsoft.com/dotnet/api/microsoft.build.tasks.deployment.manifestutilities.basereference.resolvedpath?view=msbuild-17-netcore#microsoft-build-tasks-deployment-manifestutilities-basereference.resolvedpath). If the path is empty or the file does not exist, fail signing with an error message that identifies the expected path and suggests `--clickonce-signing-version 2 --no-update-clickonce-manifest` when the user only needs to re-sign the deployment manifest. 1. Read the application manifest file using `ManifestReader.ReadManifest(...)`. If reading fails or the returned manifest is not an [`ApplicationManifest`](https://learn.microsoft.com/dotnet/api/microsoft.build.tasks.deployment.manifestutilities.applicationmanifest?view=msbuild-17-netcore), fail with a clear error. Ensure `Manifest.ReadOnly` is `false`. -1. Call `ApplicationManifest.ResolveFiles()` to resolve file references, searching the application manifest's directory first, then the deployment manifest's directory as a fallback (if different). The fallback ensures that referenced files located at the deployment root, rather than alongside the application manifest, are resolved and staged correctly. Log all `OutputMessages`. Continue when diagnostics do not prevent resolving required files; otherwise fail with a clear error. -1. Copy the deployment manifest, application manifest, and files referenced by the application manifest's `AssemblyReferences` and `FileReferences` to a temporary directory, preserving their relative layout. -1. Before signing begins, temporarily rename staged files whose names end with `.deploy` to their base names (for example, `MyApp.dll.deploy` → `MyApp.dll`). -1. Before signing or updating manifest metadata, ensure that the application manifest's references resolve to the corresponding staged files after any `.deploy` suffixes have been removed. No `ResolvedPath` used by `UpdateFileInfo(...)` may identify a source file outside the staging directory. How the implementation establishes the staged paths is an implementation detail. Fail if any required staged file cannot be resolved. +1. Identify each application-manifest `AssemblyReference` and `FileReference` that represents a physical file published with the application, following the file-extension mapping rules above and searching the application manifest's directory first, then the deployment manifest's directory as a fallback (if different). Log all `OutputMessages` produced by resolution attempts. Continue when diagnostics do not prevent locating required files; otherwise fail with a clear error. +1. Copy the deployment manifest, application manifest, and located payload files to a temporary directory. Stage each payload at its manifest target path relative to the staged application manifest while preserving any mapping-added suffix. +1. Temporarily remove mapping-added suffixes from the staged payloads. +1. Before signing or updating manifest metadata, ensure that the application manifest's references resolve to the corresponding staged files. No `ResolvedPath` used by `UpdateFileInfo(...)` may identify a source file outside the staging directory. How the implementation establishes the staged paths is an implementation detail. Fail if any required staged file cannot be resolved. 1. Discover applicable adjacent executables by checking if `setup.exe` or `Launcher.exe` exists in the same directory as the deployment manifest. `setup.exe` is an optional prerequisite bootstrapper; `Launcher.exe` launches the .NET application but does not participate in ClickOnce activation. A launcher or bootstrapper in a different directory or with a different name is not implicitly discovered as a dependency, but it will still be signed through standard Authenticode signing if matched by the user's file patterns. 1. Sign payload files. -1. Call `ApplicationManifest.UpdateFileInfo("v4.5")` to refresh payload SHA-256 hashes, sizes, and identities, then restore the `.deploy` suffixes. (`UpdateFileInfo(string)` hashes files at their `ResolvedPath`, which does not include `.deploy`; the suffixes must be absent when hashes are computed.) Make the signed payload results available to waiting callers and complete their coordinated signing operations. Then sign the application manifest, make its signed result available, and complete its operation. +1. Call `ApplicationManifest.UpdateFileInfo("v4.5")` to refresh payload SHA-256 hashes, sizes, and identities, then restore the mapping-added suffixes. (`UpdateFileInfo(string)` hashes each reference's current `ResolvedPath`; ensure those paths identify the staged, suffix-stripped payloads before calling it.) Make the signed payload results available to waiting callers and complete their coordinated signing operations. Then sign the application manifest, make its signed result available, and complete its operation. 1. Signing the application manifest changes its signed bytes and can change its assembly identity, including its public key token. Ensure that the deployment manifest's entry-point `ResolvedPath` identifies the signed staged application manifest, then call `DeployManifest.UpdateFileInfo("v4.5")` to refresh the entry-point reference's SHA-256 hash and size. Ensure that the entry-point identity matches the signed application manifest before signing the deployment manifest. When signing the deployment manifest with `mage.exe -update`, the `-appm` parameter performs this entry-point update. Make the signed deployment manifest available and complete its operation. 1. Sign applicable adjacent executables, make their signed results available, and complete their operations. 1. Copy any remaining signed files back to their original locations and clean up the staging directory. @@ -250,12 +252,12 @@ Here are two examples of how the current algorithm overcopies and oversigns. 1. Read the file using `ManifestReader.ReadManifest(...)`. If reading fails or the returned manifest is not an `ApplicationManifest`, fail the operation without signing the file. 1. Ensure `Manifest.ReadOnly` is `false`. -1. Call `ApplicationManifest.ResolveFiles()` relative to the application manifest's directory. Log all `OutputMessages`. Continue when diagnostics do not prevent resolving required files; otherwise fail with a clear error. -1. Copy the application manifest and files referenced by its `AssemblyReferences` and `FileReferences` to a temporary directory, preserving their relative layout. -1. Temporarily rename staged files whose names end with `.deploy` to their base names. -1. Before signing or updating manifest metadata, ensure that the application manifest's references resolve to the corresponding staged files after any `.deploy` suffixes have been removed. No `ResolvedPath` used by `UpdateFileInfo(...)` may identify a source file outside the staging directory. How the implementation establishes the staged paths is an implementation detail. Fail if any required staged file cannot be resolved. +1. Identify each `AssemblyReference` and `FileReference` that represents a physical file published with the application. Look for its manifest target path relative to the application manifest's directory first; if that path does not exist, look for the same path with one additional `.deploy` suffix and record that suffix as mapping-added. Log all `OutputMessages` produced by resolution attempts. Continue when diagnostics do not prevent locating required files; otherwise fail with a clear error. +1. Copy the application manifest and located payload files to a temporary directory. Stage each payload at its manifest target path relative to the staged application manifest while preserving any mapping-added suffix. +1. Temporarily remove mapping-added suffixes from the staged payloads. +1. Before signing or updating manifest metadata, ensure that the application manifest's references resolve to the corresponding staged files. No `ResolvedPath` used by `UpdateFileInfo(...)` may identify a source file outside the staging directory. How the implementation establishes the staged paths is an implementation detail. Fail if any required staged file cannot be resolved. 1. Sign payload files. -1. Call `ApplicationManifest.UpdateFileInfo("v4.5")`, restore the `.deploy` suffixes, make the signed payload results available, and complete their coordinated signing operations. +1. Call `ApplicationManifest.UpdateFileInfo("v4.5")`, restore the mapping-added suffixes, make the signed payload results available, and complete their coordinated signing operations. 1. Sign the application manifest, make its signed result available, and complete its operation. 1. Copy any remaining signed files back to their original locations and clean up the staging directory. @@ -266,8 +268,8 @@ When `--no-sign-clickonce-deps` is specified, Sign CLI will update and sign only 1. Before processing any file, obtain or wait for its coordinated signing operation. 1. If both a deployment manifest and its referenced application manifest are explicitly provided in the same invocation, update and sign the application manifest first, regardless of input order or parallel scheduling. The deployment-manifest operation must wait for the application-manifest operation to complete successfully before refreshing its entry-point metadata and signing. If the application-manifest operation fails, do not sign the deployment manifest. 1. For each file provided by the user: - - If the file has a `.vsto` or `.application` file extension, read it as a deployment manifest and call `DeployManifest.ResolveFiles()`. Call `DeployManifest.UpdateFileInfo("v4.5")` to refresh the entry-point reference's SHA-256 hash and size, ensure that the entry-point identity matches the referenced application manifest's current identity, then sign only the deployment manifest. - - If the file has a `.manifest` file extension, read it as an application manifest and call `ApplicationManifest.ResolveFiles()`. Temporarily remove `.deploy` suffixes from referenced payloads, call `ApplicationManifest.UpdateFileInfo("v4.5")`, restore the suffixes, then sign only the application manifest. + - If the file has a `.vsto` or `.application` file extension, read it as a deployment manifest and call `DeployManifest.ResolveFiles(string[])` with the deployment manifest's directory. Call `DeployManifest.UpdateFileInfo("v4.5")` to refresh the entry-point reference's SHA-256 hash and size, ensure that the entry-point identity matches the referenced application manifest's current identity, then sign only the deployment manifest. + - If the file has a `.manifest` file extension, follow the standalone application-manifest discovery, staging, resolution, and update steps above, but skip payload signing and do not copy staged payloads back. Sign and copy back only the application manifest. - For other file types, apply the standard signing logic. 1. Complete each coordinated signing operation after its file is signed. 1. Referenced manifests and payload files are discovered during the update process but are not signed. From 0fed394f10b14e14aa5c90491612fdc035266eb1 Mon Sep 17 00:00:00 2001 From: Damon Tivel Date: Wed, 5 Aug 2026 18:18:53 -0700 Subject: [PATCH 12/17] Clarify ClickOnce launcher handling Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: e7d1383c-affb-4651-8d11-9bd70d5446c3 --- docs/specs/ClickOnce-Signing-Algorithm.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/specs/ClickOnce-Signing-Algorithm.md b/docs/specs/ClickOnce-Signing-Algorithm.md index b992a471..9111687f 100644 --- a/docs/specs/ClickOnce-Signing-Algorithm.md +++ b/docs/specs/ClickOnce-Signing-Algorithm.md @@ -15,7 +15,7 @@ A ClickOnce application consists of: Published output may also include: * optionally, a `setup.exe` bootstrapper for installing prerequisite packages before the ClickOnce application. -* optionally, a `Launcher.exe` file for launching the .NET application. `Launcher.exe` does not participate in ClickOnce activation. +* for launcher-based .NET ClickOnce applications, a `Launcher.exe` file that the application manifest identifies as its entry point. The ClickOnce runtime launches `Launcher.exe`, and `Launcher.exe` starts the .NET application; `Launcher.exe` does not itself implement ClickOnce activation or deployment logic. Some publish layouts may also contain a separate root-level copy that is not referenced by the application manifest. Publishing a ClickOnce application generates deployment and application manifests and payload files, and may also generate a bootstrapper or launcher. In a typical Visual Studio publish layout, the deployment manifest is in the parent publish directory, while the application manifest and payload files are in a version-specific child directory. The deployment manifest points to the application manifest for the current version. Other valid layouts may organize these files differently. @@ -45,7 +45,7 @@ There are two special cases that complicate signing: Given a deployment manifest file as a starting point, the algorithm will be updated to: 1. Load the deployment manifest, locate the referenced application manifest, and, by default, refuse to continue if it is missing. -1. Stage only the files referenced by the manifests and any applicable adjacent `setup.exe` or `Launcher.exe`. +1. Stage only the files referenced by the manifests and any applicable adjacent executables. A `Launcher.exe` referenced by the application manifest is a payload file; a separate root-level `Launcher.exe` may be treated as an adjacent executable. 1. Sign the payloads, refresh the application manifest's metadata, and sign the application manifest. Then refresh the deployment manifest's entry-point metadata, sign the deployment manifest, and finally sign the adjacent executables so hashes, sizes, identities, and entry-point information are consistent with the newly signed files. Implementation specifics, including path resolution, `.deploy` renaming, and `ManifestUtilities` API calls, are detailed in Appendix B. @@ -241,7 +241,7 @@ When `DeployManifest.MapFileExtensions` is `true`, ClickOnce file-extension mapp 1. Copy the deployment manifest, application manifest, and located payload files to a temporary directory. Stage each payload at its manifest target path relative to the staged application manifest while preserving any mapping-added suffix. 1. Temporarily remove mapping-added suffixes from the staged payloads. 1. Before signing or updating manifest metadata, ensure that the application manifest's references resolve to the corresponding staged files. No `ResolvedPath` used by `UpdateFileInfo(...)` may identify a source file outside the staging directory. How the implementation establishes the staged paths is an implementation detail. Fail if any required staged file cannot be resolved. -1. Discover applicable adjacent executables by checking if `setup.exe` or `Launcher.exe` exists in the same directory as the deployment manifest. `setup.exe` is an optional prerequisite bootstrapper; `Launcher.exe` launches the .NET application but does not participate in ClickOnce activation. A launcher or bootstrapper in a different directory or with a different name is not implicitly discovered as a dependency, but it will still be signed through standard Authenticode signing if matched by the user's file patterns. +1. Treat any `Launcher.exe` referenced by the application manifest, including as its entry point, as a payload file. It must be staged and signed with the other signable payloads before application-manifest metadata is refreshed. Separately discover applicable adjacent executables by checking if `setup.exe` or an unreferenced `Launcher.exe` exists in the same directory as the deployment manifest. A launcher or bootstrapper in a different directory or with a different name is not implicitly discovered as an adjacent executable, but it will still be signed through standard Authenticode signing if matched by the user's file patterns. 1. Sign payload files. 1. Call `ApplicationManifest.UpdateFileInfo("v4.5")` to refresh payload SHA-256 hashes, sizes, and identities, then restore the mapping-added suffixes. (`UpdateFileInfo(string)` hashes each reference's current `ResolvedPath`; ensure those paths identify the staged, suffix-stripped payloads before calling it.) Make the signed payload results available to waiting callers and complete their coordinated signing operations. Then sign the application manifest, make its signed result available, and complete its operation. 1. Signing the application manifest changes its signed bytes and can change its assembly identity, including its public key token. Ensure that the deployment manifest's entry-point `ResolvedPath` identifies the signed staged application manifest, then call `DeployManifest.UpdateFileInfo("v4.5")` to refresh the entry-point reference's SHA-256 hash and size. Ensure that the entry-point identity matches the signed application manifest before signing the deployment manifest. When signing the deployment manifest with `mage.exe -update`, the `-appm` parameter performs this entry-point update. Make the signed deployment manifest available and complete its operation. From 3eb6ae4f88abbf45e4da001a4f1a76a6a2ff7a9c Mon Sep 17 00:00:00 2001 From: Damon Tivel Date: Wed, 5 Aug 2026 18:21:51 -0700 Subject: [PATCH 13/17] Clarify application manifest identity refresh Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: e7d1383c-affb-4651-8d11-9bd70d5446c3 --- docs/specs/ClickOnce-Signing-Algorithm.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/specs/ClickOnce-Signing-Algorithm.md b/docs/specs/ClickOnce-Signing-Algorithm.md index 9111687f..c08648d1 100644 --- a/docs/specs/ClickOnce-Signing-Algorithm.md +++ b/docs/specs/ClickOnce-Signing-Algorithm.md @@ -219,6 +219,8 @@ Here are two examples of how the current algorithm overcopies and oversigns. `Manifest` provides a parameterless `UpdateFileInfo()` overload and an `UpdateFileInfo(string targetFrameworkVersion)` overload. Version 2 does not call the parameterless overload, which computes SHA-1 hashes for referenced files. Whenever version 2 updates file information, it calls `UpdateFileInfo("v4.5")`. MSBuild's manifest utility implementation selects SHA-256 when the supplied target framework version is greater than `"v4.0"`; `"v4.5"` selects the hashing behavior and does not represent the application's target framework. This preserves Sign CLI's current SHA-256 behavior and does not add support for ClickOnce runtimes that require SHA-1 manifest hashes. +Current MSBuild derives an assembly-reference identity during `UpdateFileInfo(...)` only when the identity is unspecified; it does not replace an existing stale identity. Before signing an application manifest, version 2 ensures that each assembly reference representing a staged payload has an identity that matches that payload. How the implementation synchronizes those identities is an implementation detail. + When `DeployManifest.MapFileExtensions` is `true`, ClickOnce file-extension mapping leaves manifest target paths unchanged and appends one additional `.deploy` suffix to physical published payload files. Whenever version 2 stages a mapped payload, it preserves the physical filename, records that the additional suffix was mapped, and copies the source without renaming it. Before resolving staged references or calling `UpdateFileInfo(...)`, temporarily remove only the recorded suffix from the staged copy, then restore it afterward. Do not remove a `.deploy` suffix that is part of the manifest target path. ### Default behavior (no dependency options) @@ -243,7 +245,7 @@ When `DeployManifest.MapFileExtensions` is `true`, ClickOnce file-extension mapp 1. Before signing or updating manifest metadata, ensure that the application manifest's references resolve to the corresponding staged files. No `ResolvedPath` used by `UpdateFileInfo(...)` may identify a source file outside the staging directory. How the implementation establishes the staged paths is an implementation detail. Fail if any required staged file cannot be resolved. 1. Treat any `Launcher.exe` referenced by the application manifest, including as its entry point, as a payload file. It must be staged and signed with the other signable payloads before application-manifest metadata is refreshed. Separately discover applicable adjacent executables by checking if `setup.exe` or an unreferenced `Launcher.exe` exists in the same directory as the deployment manifest. A launcher or bootstrapper in a different directory or with a different name is not implicitly discovered as an adjacent executable, but it will still be signed through standard Authenticode signing if matched by the user's file patterns. 1. Sign payload files. -1. Call `ApplicationManifest.UpdateFileInfo("v4.5")` to refresh payload SHA-256 hashes, sizes, and identities, then restore the mapping-added suffixes. (`UpdateFileInfo(string)` hashes each reference's current `ResolvedPath`; ensure those paths identify the staged, suffix-stripped payloads before calling it.) Make the signed payload results available to waiting callers and complete their coordinated signing operations. Then sign the application manifest, make its signed result available, and complete its operation. +1. Call `ApplicationManifest.UpdateFileInfo("v4.5")` to refresh payload SHA-256 hashes and sizes, then restore the mapping-added suffixes. (`UpdateFileInfo(string)` hashes each reference's current `ResolvedPath`; ensure those paths identify the staged, suffix-stripped payloads before calling it.) Make the signed payload results available to waiting callers and complete their coordinated signing operations. Then sign the application manifest, make its signed result available, and complete its operation. 1. Signing the application manifest changes its signed bytes and can change its assembly identity, including its public key token. Ensure that the deployment manifest's entry-point `ResolvedPath` identifies the signed staged application manifest, then call `DeployManifest.UpdateFileInfo("v4.5")` to refresh the entry-point reference's SHA-256 hash and size. Ensure that the entry-point identity matches the signed application manifest before signing the deployment manifest. When signing the deployment manifest with `mage.exe -update`, the `-appm` parameter performs this entry-point update. Make the signed deployment manifest available and complete its operation. 1. Sign applicable adjacent executables, make their signed results available, and complete their operations. 1. Copy any remaining signed files back to their original locations and clean up the staging directory. From bae29c382d2d18479dc3d06973312e477f53f20f Mon Sep 17 00:00:00 2001 From: Damon Tivel Date: Wed, 5 Aug 2026 18:37:43 -0700 Subject: [PATCH 14/17] Clarify deployment-only ClickOnce signing Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: e7d1383c-affb-4651-8d11-9bd70d5446c3 --- docs/specs/ClickOnce-Signing-Algorithm.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/specs/ClickOnce-Signing-Algorithm.md b/docs/specs/ClickOnce-Signing-Algorithm.md index c08648d1..9901bc55 100644 --- a/docs/specs/ClickOnce-Signing-Algorithm.md +++ b/docs/specs/ClickOnce-Signing-Algorithm.md @@ -145,13 +145,13 @@ sign code certificate-store ... --clickonce-signing-version 2 -b Output\ **/*.vs Each `.vsto` file is processed independently: its referenced application manifest and payload files are discovered and signed. Signing operations for shared files are coordinated so each file is signed only once and all dependents wait for signing to complete. -#### Re-sign only a deployment manifest (after payload changes) +#### Re-sign only a deployment manifest when the application manifest is already current ```shell sign code certificate-store ... --clickonce-signing-version 2 --no-sign-clickonce-deps -b publish\ App.application ``` -Updates the deployment manifest's metadata (sizes, hashes) to reflect the current state of its dependencies, then signs only the deployment manifest. Dependencies are not signed. +Refreshes the deployment manifest's hash, size, and identity for the current referenced application manifest, then signs only the deployment manifest. It does not update payload metadata in the application manifest or sign any dependencies. If payloads changed, update and sign the application manifest first, then update and sign the deployment manifest. #### Re-sign a manifest without updating metadata From 423e38e8e5dd8a0df4596f5508c6a7878acb5ea3 Mon Sep 17 00:00:00 2001 From: Damon Tivel Date: Wed, 5 Aug 2026 18:49:15 -0700 Subject: [PATCH 15/17] Clarify caller responsibility for manifest consistency Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: e7d1383c-affb-4651-8d11-9bd70d5446c3 --- docs/specs/ClickOnce-Signing-Algorithm.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/docs/specs/ClickOnce-Signing-Algorithm.md b/docs/specs/ClickOnce-Signing-Algorithm.md index 9901bc55..2004fc1b 100644 --- a/docs/specs/ClickOnce-Signing-Algorithm.md +++ b/docs/specs/ClickOnce-Signing-Algorithm.md @@ -161,6 +161,8 @@ sign code certificate-store ... --clickonce-signing-version 2 --no-update-clicko Signs the deployment manifest as-is, without resolving files or updating file information. Useful when re-signing with a different certificate and dependencies have not changed. +When using this option to sign an application manifest, the caller is responsible for updating and re-signing any deployment manifest whose reference is invalidated by the application manifest's changed contents. + ## Appendix A: Signing algorithm version 1 In a temporary directory: @@ -288,12 +290,12 @@ When `--no-update-clickonce-manifest` is specified, Sign CLI will sign manifest - For other file types, apply the standard signing logic. 1. Complete each coordinated signing operation after its file is signed. 1. No ClickOnce dependency discovery or manifest metadata updates occur. -1. This option is useful when re-signing manifests whose dependencies have not changed. +1. This option is useful when re-signing manifests whose dependencies have not changed. Re-signing an application manifest changes its contents and may invalidate deployment manifests that reference it; the caller is responsible for updating and re-signing related manifests as needed. ### Option interactions The `--no-sign-clickonce-deps` and `--no-update-clickonce-manifest` options are mutually exclusive: * `--no-sign-clickonce-deps` alone: Update and sign only specified manifests (dependencies discovered but not signed). -* `--no-update-clickonce-manifest` alone: Sign only specified manifests without updating them (no discovery of dependencies). This is the fastest option, but the user must ensure manifests are already consistent with their dependencies. +* `--no-update-clickonce-manifest` alone: Sign only specified manifests without updating them (no discovery of dependencies). This is the fastest option, but the caller is responsible for maintaining consistency among manifests and their dependencies. * Both options together: Not allowed. `--no-update-clickonce-manifest` skips all discovery and metadata updates, which fully subsumes the dependency-skipping behavior of `--no-sign-clickonce-deps`. Sign CLI will emit an error: `The '--no-sign-clickonce-deps' and '--no-update-clickonce-manifest' options cannot be combined.` From 1ce988c290a7ea2e56ef741bf624db5163c14115 Mon Sep 17 00:00:00 2001 From: Damon Tivel Date: Wed, 5 Aug 2026 18:59:26 -0700 Subject: [PATCH 16/17] Clarify explicit application manifest inputs Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: e7d1383c-affb-4651-8d11-9bd70d5446c3 --- docs/specs/ClickOnce-Signing-Algorithm.md | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/docs/specs/ClickOnce-Signing-Algorithm.md b/docs/specs/ClickOnce-Signing-Algorithm.md index 2004fc1b..61c484e1 100644 --- a/docs/specs/ClickOnce-Signing-Algorithm.md +++ b/docs/specs/ClickOnce-Signing-Algorithm.md @@ -225,12 +225,14 @@ Current MSBuild derives an assembly-reference identity during `UpdateFileInfo(.. When `DeployManifest.MapFileExtensions` is `true`, ClickOnce file-extension mapping leaves manifest target paths unchanged and appends one additional `.deploy` suffix to physical published payload files. Whenever version 2 stages a mapped payload, it preserves the physical filename, records that the additional suffix was mapped, and copies the source without renaming it. Before resolving staged references or calling `UpdateFileInfo(...)`, temporarily remove only the recorded suffix from the staged copy, then restore it afterward. Do not remove a `.deploy` suffix that is part of the manifest target path. +Apply the application-manifest flows below only to files identified as ClickOnce application manifests. A different manifest format using the `.manifest` extension must not be treated as ClickOnce or fail merely because signing algorithm version 2 is enabled; handle it through the standard signing behavior for its format. + ### Default behavior (no dependency options) 1. Before staging or signing any file, obtain the coordinated signing operation for its canonical source path (via `Path.GetFullPath()`). The first caller owns the operation; duplicate callers wait for its result before staging or consuming the file. 1. Determine the file type and read the manifest: - For a `.vsto` or `.application` file, follow [Deployment-manifest input](#deployment-manifest-input). - - For a `.manifest` file, follow [Standalone application-manifest input](#standalone-application-manifest-input). + - For an explicitly provided ClickOnce application manifest, follow [Explicit application-manifest input](#explicit-application-manifest-input). - For any other file type, apply the standard signing logic. #### Deployment-manifest input @@ -252,7 +254,7 @@ When `DeployManifest.MapFileExtensions` is `true`, ClickOnce file-extension mapp 1. Sign applicable adjacent executables, make their signed results available, and complete their operations. 1. Copy any remaining signed files back to their original locations and clean up the staging directory. -#### Standalone application-manifest input +#### Explicit application-manifest input 1. Read the file using `ManifestReader.ReadManifest(...)`. If reading fails or the returned manifest is not an `ApplicationManifest`, fail the operation without signing the file. 1. Ensure `Manifest.ReadOnly` is `false`. @@ -273,7 +275,7 @@ When `--no-sign-clickonce-deps` is specified, Sign CLI will update and sign only 1. If both a deployment manifest and its referenced application manifest are explicitly provided in the same invocation, update and sign the application manifest first, regardless of input order or parallel scheduling. The deployment-manifest operation must wait for the application-manifest operation to complete successfully before refreshing its entry-point metadata and signing. If the application-manifest operation fails, do not sign the deployment manifest. 1. For each file provided by the user: - If the file has a `.vsto` or `.application` file extension, read it as a deployment manifest and call `DeployManifest.ResolveFiles(string[])` with the deployment manifest's directory. Call `DeployManifest.UpdateFileInfo("v4.5")` to refresh the entry-point reference's SHA-256 hash and size, ensure that the entry-point identity matches the referenced application manifest's current identity, then sign only the deployment manifest. - - If the file has a `.manifest` file extension, follow the standalone application-manifest discovery, staging, resolution, and update steps above, but skip payload signing and do not copy staged payloads back. Sign and copy back only the application manifest. + - If the file is a ClickOnce application manifest, follow the explicit application-manifest discovery, staging, resolution, and update steps above, but skip payload signing and do not copy staged payloads back. Sign and copy back only the application manifest. - For other file types, apply the standard signing logic. 1. Complete each coordinated signing operation after its file is signed. 1. Referenced manifests and payload files are discovered during the update process but are not signed. @@ -286,7 +288,7 @@ When `--no-update-clickonce-manifest` is specified, Sign CLI will sign manifest 1. Before processing any file, obtain or wait for its coordinated signing operation. 1. For each file provided by the user: - If the file has a `.vsto` or `.application` file extension, read it as a deployment manifest and sign it without resolving files or updating file information. - - If the file has a `.manifest` file extension, read it as an application manifest and sign it without resolving files or updating file information. + - If the file is a ClickOnce application manifest, sign it without resolving files or updating file information. - For other file types, apply the standard signing logic. 1. Complete each coordinated signing operation after its file is signed. 1. No ClickOnce dependency discovery or manifest metadata updates occur. From e1020634d57f2586211a03ee404b3ac7e990ae6e Mon Sep 17 00:00:00 2001 From: Damon Tivel Date: Wed, 5 Aug 2026 19:07:30 -0700 Subject: [PATCH 17/17] Clarify signing of ClickOnce payload formats Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: e7d1383c-affb-4651-8d11-9bd70d5446c3 --- docs/specs/ClickOnce-Signing-Algorithm.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/specs/ClickOnce-Signing-Algorithm.md b/docs/specs/ClickOnce-Signing-Algorithm.md index 61c484e1..02bced81 100644 --- a/docs/specs/ClickOnce-Signing-Algorithm.md +++ b/docs/specs/ClickOnce-Signing-Algorithm.md @@ -248,7 +248,7 @@ Apply the application-manifest flows below only to files identified as ClickOnce 1. Temporarily remove mapping-added suffixes from the staged payloads. 1. Before signing or updating manifest metadata, ensure that the application manifest's references resolve to the corresponding staged files. No `ResolvedPath` used by `UpdateFileInfo(...)` may identify a source file outside the staging directory. How the implementation establishes the staged paths is an implementation detail. Fail if any required staged file cannot be resolved. 1. Treat any `Launcher.exe` referenced by the application manifest, including as its entry point, as a payload file. It must be staged and signed with the other signable payloads before application-manifest metadata is refreshed. Separately discover applicable adjacent executables by checking if `setup.exe` or an unreferenced `Launcher.exe` exists in the same directory as the deployment manifest. A launcher or bootstrapper in a different directory or with a different name is not implicitly discovered as an adjacent executable, but it will still be signed through standard Authenticode signing if matched by the user's file patterns. -1. Sign payload files. +1. Apply the standard signing behavior to each staged payload file. Sign supported file formats and leave unsupported files unchanged. Include every referenced payload when refreshing application-manifest metadata. 1. Call `ApplicationManifest.UpdateFileInfo("v4.5")` to refresh payload SHA-256 hashes and sizes, then restore the mapping-added suffixes. (`UpdateFileInfo(string)` hashes each reference's current `ResolvedPath`; ensure those paths identify the staged, suffix-stripped payloads before calling it.) Make the signed payload results available to waiting callers and complete their coordinated signing operations. Then sign the application manifest, make its signed result available, and complete its operation. 1. Signing the application manifest changes its signed bytes and can change its assembly identity, including its public key token. Ensure that the deployment manifest's entry-point `ResolvedPath` identifies the signed staged application manifest, then call `DeployManifest.UpdateFileInfo("v4.5")` to refresh the entry-point reference's SHA-256 hash and size. Ensure that the entry-point identity matches the signed application manifest before signing the deployment manifest. When signing the deployment manifest with `mage.exe -update`, the `-appm` parameter performs this entry-point update. Make the signed deployment manifest available and complete its operation. 1. Sign applicable adjacent executables, make their signed results available, and complete their operations. @@ -262,7 +262,7 @@ Apply the application-manifest flows below only to files identified as ClickOnce 1. Copy the application manifest and located payload files to a temporary directory. Stage each payload at its manifest target path relative to the staged application manifest while preserving any mapping-added suffix. 1. Temporarily remove mapping-added suffixes from the staged payloads. 1. Before signing or updating manifest metadata, ensure that the application manifest's references resolve to the corresponding staged files. No `ResolvedPath` used by `UpdateFileInfo(...)` may identify a source file outside the staging directory. How the implementation establishes the staged paths is an implementation detail. Fail if any required staged file cannot be resolved. -1. Sign payload files. +1. Apply the standard signing behavior to each staged payload file. Sign supported file formats and leave unsupported files unchanged. Include every referenced payload when refreshing application-manifest metadata. 1. Call `ApplicationManifest.UpdateFileInfo("v4.5")`, restore the mapping-added suffixes, make the signed payload results available, and complete their coordinated signing operations. 1. Sign the application manifest, make its signed result available, and complete its operation. 1. Copy any remaining signed files back to their original locations and clean up the staging directory.