diff --git a/src/Sign.Core/DataFormatSigners/ClickOnceSigner.cs b/src/Sign.Core/DataFormatSigners/ClickOnceSigner.cs
index 4dec8194..2873ed69 100644
--- a/src/Sign.Core/DataFormatSigners/ClickOnceSigner.cs
+++ b/src/Sign.Core/DataFormatSigners/ClickOnceSigner.cs
@@ -5,6 +5,7 @@
using System.Globalization;
using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;
+using System.Xml;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.FileSystemGlobbing.Abstractions;
using Microsoft.Extensions.Logging;
@@ -126,8 +127,8 @@ await Parallel.ForEachAsync(files, _parallelOptions, async (file, state) =>
// Inner files are now signed
// now look for the manifest file and sign that if we have one
-
- FileInfo? manifestFile = filteredFiles.SingleOrDefault(f => ".manifest".Equals(f.Extension, StringComparison.OrdinalIgnoreCase));
+ var appManifestFromDeploymentManifest = GetApplicationManifestForDeploymentManifest(file);
+ FileInfo? manifestFile = filteredFiles.SingleOrDefault(f => f.Name.Equals(appManifestFromDeploymentManifest, StringComparison.OrdinalIgnoreCase));
string fileArgs = $@"-update ""{manifestFile}"" {args}";
@@ -273,5 +274,53 @@ public void CopySigningDependencies(FileInfo deploymentManifestFile, DirectoryIn
}
}
}
+
+ ///
+ /// Try and find the application manifest (.manifest) file from a clickonce application manifest (.application / .vsto
+ /// There might not be one, if the user is attempting to only re-sign the deployment manifest without touching other files.
+ /// This is necessary because there might be multiple *.manifest files present, e.g. if a DLL that's part of the clickonce
+ /// package ships its own assembly manifest which isn't a clickonce application manifest.
+ ///
+ ///
+ /// A string containing the file name of the Application manifest, or null if it couldn't be found.
+ ///
+ private string? GetApplicationManifestForDeploymentManifest(FileInfo deploymentManifest)
+ {
+ var xml = new XmlDocument();
+ xml.Load(deploymentManifest.FullName);
+ // there should only be a single result here, if the file is a valid clickonce manifest.
+ var dependentAssemblies = xml.GetElementsByTagName("dependentAssembly");
+ if (dependentAssemblies.Count != 1)
+ {
+ Logger.LogDebug(Resources.ApplicationManifestNotFound);
+ return null;
+ }
+
+ var node = dependentAssemblies.Item(0);
+ if (node is null || node.Attributes is null)
+ {
+ Logger.LogDebug(Resources.ApplicationManifestNotFound);
+ return null;
+ }
+
+ var applicationManifestFileNameAttribute = node.Attributes["codebase"];
+ if (applicationManifestFileNameAttribute is null || string.IsNullOrEmpty(applicationManifestFileNameAttribute.Value))
+ {
+ Logger.LogDebug(Resources.ApplicationManifestNotFound);
+ return null;
+ }
+
+ // the codebase attribute can be a relative file path (e.g. Application Files\MyApp_1_0_0_0\MyApp.dll.manifest) or
+ // a URI (e.g. https://my.cdn.com/clickonce/MyApp/ApplicationFiles/MyApp_1_0_0_0/MyApp.dll.manifest) so we need to
+ // handle both cases and extract just the file name part.
+ //
+ // we only try and parse absolute uris, because a relative uri can just be treated like a file path for our purposes
+ if (Uri.TryCreate(applicationManifestFileNameAttribute.Value, UriKind.Absolute, out var uri))
+ {
+ Path.GetFileName(uri.LocalPath); // works for http(s) and file:// uris
+ }
+
+ return Path.GetFileName(applicationManifestFileNameAttribute.Value);
+ }
}
-}
\ No newline at end of file
+}
diff --git a/src/Sign.Core/Resources.Designer.cs b/src/Sign.Core/Resources.Designer.cs
index 2cd9c9a6..8fd38aa6 100644
--- a/src/Sign.Core/Resources.Designer.cs
+++ b/src/Sign.Core/Resources.Designer.cs
@@ -60,6 +60,15 @@ internal Resources() {
}
}
+ ///
+ /// Looks up a localized string similar to Did not find exactly 1 <dependentAssembly> element with a non-empty 'codebase' attribute within the deployment manifest..
+ ///
+ internal static string ApplicationManifestNotFound {
+ get {
+ return ResourceManager.GetString("ApplicationManifestNotFound", resourceCulture);
+ }
+ }
+
///
/// Looks up a localized string similar to Signing SignTool job with {count} files..
///
diff --git a/src/Sign.Core/Resources.resx b/src/Sign.Core/Resources.resx
index 4ca2c0db..e245e5a1 100644
--- a/src/Sign.Core/Resources.resx
+++ b/src/Sign.Core/Resources.resx
@@ -275,4 +275,7 @@
The algorithm selected is not supported.
+
+ Did not find exactly 1 <dependentAssembly> element with a non-empty 'codebase' attribute within the deployment manifest.
+
\ No newline at end of file
diff --git a/src/Sign.Core/xlf/Resources.cs.xlf b/src/Sign.Core/xlf/Resources.cs.xlf
index 10eedd92..91d1048e 100644
--- a/src/Sign.Core/xlf/Resources.cs.xlf
+++ b/src/Sign.Core/xlf/Resources.cs.xlf
@@ -2,6 +2,11 @@
+
+ Did not find exactly 1 <dependentAssembly> element with a non-empty 'codebase' attribute within the deployment manifest.
+ Did not find exactly 1 <dependentAssembly> element with a non-empty 'codebase' attribute within the deployment manifest.
+
+ Signing SignTool job with {count} files.Podepisování úlohy SignTool s tímto počtem souborů: {count}.
diff --git a/src/Sign.Core/xlf/Resources.de.xlf b/src/Sign.Core/xlf/Resources.de.xlf
index 36fe225f..438f85b9 100644
--- a/src/Sign.Core/xlf/Resources.de.xlf
+++ b/src/Sign.Core/xlf/Resources.de.xlf
@@ -2,6 +2,11 @@
+
+ Did not find exactly 1 <dependentAssembly> element with a non-empty 'codebase' attribute within the deployment manifest.
+ Did not find exactly 1 <dependentAssembly> element with a non-empty 'codebase' attribute within the deployment manifest.
+
+ Signing SignTool job with {count} files.Der SignTool-Auftrag wird mit {count} Dateien signiert.
diff --git a/src/Sign.Core/xlf/Resources.es.xlf b/src/Sign.Core/xlf/Resources.es.xlf
index 5f568dfa..449e1c8c 100644
--- a/src/Sign.Core/xlf/Resources.es.xlf
+++ b/src/Sign.Core/xlf/Resources.es.xlf
@@ -2,6 +2,11 @@
+
+ Did not find exactly 1 <dependentAssembly> element with a non-empty 'codebase' attribute within the deployment manifest.
+ Did not find exactly 1 <dependentAssembly> element with a non-empty 'codebase' attribute within the deployment manifest.
+
+ Signing SignTool job with {count} files.Firmando el trabajo de SignTool con {count} archivos.
diff --git a/src/Sign.Core/xlf/Resources.fr.xlf b/src/Sign.Core/xlf/Resources.fr.xlf
index 69dcaead..16a6fdc7 100644
--- a/src/Sign.Core/xlf/Resources.fr.xlf
+++ b/src/Sign.Core/xlf/Resources.fr.xlf
@@ -2,6 +2,11 @@
+
+ Did not find exactly 1 <dependentAssembly> element with a non-empty 'codebase' attribute within the deployment manifest.
+ Did not find exactly 1 <dependentAssembly> element with a non-empty 'codebase' attribute within the deployment manifest.
+
+ Signing SignTool job with {count} files.Signature du travail SignTool avec {count} fichiers.
diff --git a/src/Sign.Core/xlf/Resources.it.xlf b/src/Sign.Core/xlf/Resources.it.xlf
index 203f3a25..950c4155 100644
--- a/src/Sign.Core/xlf/Resources.it.xlf
+++ b/src/Sign.Core/xlf/Resources.it.xlf
@@ -2,6 +2,11 @@
+
+ Did not find exactly 1 <dependentAssembly> element with a non-empty 'codebase' attribute within the deployment manifest.
+ Did not find exactly 1 <dependentAssembly> element with a non-empty 'codebase' attribute within the deployment manifest.
+
+ Signing SignTool job with {count} files.Firma del processo SignTool con {count} file.
diff --git a/src/Sign.Core/xlf/Resources.ja.xlf b/src/Sign.Core/xlf/Resources.ja.xlf
index db44e64d..d55bf2c1 100644
--- a/src/Sign.Core/xlf/Resources.ja.xlf
+++ b/src/Sign.Core/xlf/Resources.ja.xlf
@@ -2,6 +2,11 @@
+
+ Did not find exactly 1 <dependentAssembly> element with a non-empty 'codebase' attribute within the deployment manifest.
+ Did not find exactly 1 <dependentAssembly> element with a non-empty 'codebase' attribute within the deployment manifest.
+
+ Signing SignTool job with {count} files.{count} 個のファイルを使用して SignTool ジョブに署名しています。
diff --git a/src/Sign.Core/xlf/Resources.ko.xlf b/src/Sign.Core/xlf/Resources.ko.xlf
index cf25df6f..fe2390d1 100644
--- a/src/Sign.Core/xlf/Resources.ko.xlf
+++ b/src/Sign.Core/xlf/Resources.ko.xlf
@@ -2,6 +2,11 @@
+
+ Did not find exactly 1 <dependentAssembly> element with a non-empty 'codebase' attribute within the deployment manifest.
+ Did not find exactly 1 <dependentAssembly> element with a non-empty 'codebase' attribute within the deployment manifest.
+
+ Signing SignTool job with {count} files.{count} 파일로 SignTool 작업에 서명하는 중입니다.
diff --git a/src/Sign.Core/xlf/Resources.pl.xlf b/src/Sign.Core/xlf/Resources.pl.xlf
index c9db68da..012cdd71 100644
--- a/src/Sign.Core/xlf/Resources.pl.xlf
+++ b/src/Sign.Core/xlf/Resources.pl.xlf
@@ -2,6 +2,11 @@
+
+ Did not find exactly 1 <dependentAssembly> element with a non-empty 'codebase' attribute within the deployment manifest.
+ Did not find exactly 1 <dependentAssembly> element with a non-empty 'codebase' attribute within the deployment manifest.
+
+ Signing SignTool job with {count} files.Podpisywanie zadania SignTool przy użyciu {count} plików.
diff --git a/src/Sign.Core/xlf/Resources.pt-BR.xlf b/src/Sign.Core/xlf/Resources.pt-BR.xlf
index a405b988..077a9235 100644
--- a/src/Sign.Core/xlf/Resources.pt-BR.xlf
+++ b/src/Sign.Core/xlf/Resources.pt-BR.xlf
@@ -2,6 +2,11 @@
+
+ Did not find exactly 1 <dependentAssembly> element with a non-empty 'codebase' attribute within the deployment manifest.
+ Did not find exactly 1 <dependentAssembly> element with a non-empty 'codebase' attribute within the deployment manifest.
+
+ Signing SignTool job with {count} files.Autenticando o trabalho SignTool com {count} arquivos.
diff --git a/src/Sign.Core/xlf/Resources.ru.xlf b/src/Sign.Core/xlf/Resources.ru.xlf
index 13aaacb1..0ad097de 100644
--- a/src/Sign.Core/xlf/Resources.ru.xlf
+++ b/src/Sign.Core/xlf/Resources.ru.xlf
@@ -2,6 +2,11 @@
+
+ Did not find exactly 1 <dependentAssembly> element with a non-empty 'codebase' attribute within the deployment manifest.
+ Did not find exactly 1 <dependentAssembly> element with a non-empty 'codebase' attribute within the deployment manifest.
+
+ Signing SignTool job with {count} files.Задание подписывания SignTool с несколькими файлами ({count}).
diff --git a/src/Sign.Core/xlf/Resources.tr.xlf b/src/Sign.Core/xlf/Resources.tr.xlf
index aac32f14..d935eb59 100644
--- a/src/Sign.Core/xlf/Resources.tr.xlf
+++ b/src/Sign.Core/xlf/Resources.tr.xlf
@@ -2,6 +2,11 @@
+
+ Did not find exactly 1 <dependentAssembly> element with a non-empty 'codebase' attribute within the deployment manifest.
+ Did not find exactly 1 <dependentAssembly> element with a non-empty 'codebase' attribute within the deployment manifest.
+
+ Signing SignTool job with {count} files.SignTool işi {count} dosya ile imzalanıyor.
diff --git a/src/Sign.Core/xlf/Resources.zh-Hans.xlf b/src/Sign.Core/xlf/Resources.zh-Hans.xlf
index cf8ecb11..405fe481 100644
--- a/src/Sign.Core/xlf/Resources.zh-Hans.xlf
+++ b/src/Sign.Core/xlf/Resources.zh-Hans.xlf
@@ -2,6 +2,11 @@
+
+ Did not find exactly 1 <dependentAssembly> element with a non-empty 'codebase' attribute within the deployment manifest.
+ Did not find exactly 1 <dependentAssembly> element with a non-empty 'codebase' attribute within the deployment manifest.
+
+ Signing SignTool job with {count} files.正在对包含 {count} 个文件的 SignTool 作业进行签名。
diff --git a/src/Sign.Core/xlf/Resources.zh-Hant.xlf b/src/Sign.Core/xlf/Resources.zh-Hant.xlf
index 1fb97a2e..e39345aa 100644
--- a/src/Sign.Core/xlf/Resources.zh-Hant.xlf
+++ b/src/Sign.Core/xlf/Resources.zh-Hant.xlf
@@ -2,6 +2,11 @@
+
+ Did not find exactly 1 <dependentAssembly> element with a non-empty 'codebase' attribute within the deployment manifest.
+ Did not find exactly 1 <dependentAssembly> element with a non-empty 'codebase' attribute within the deployment manifest.
+
+ Signing SignTool job with {count} files.正在簽署具有 {count} 個檔案的 SignTool 工作。
diff --git a/test/Sign.Core.Test/SignatureProviders/ClickOnceSignerTests.cs b/test/Sign.Core.Test/SignatureProviders/ClickOnceSignerTests.cs
index c906f771..bf665eee 100644
--- a/test/Sign.Core.Test/SignatureProviders/ClickOnceSignerTests.cs
+++ b/test/Sign.Core.Test/SignatureProviders/ClickOnceSignerTests.cs
@@ -14,6 +14,22 @@ public sealed class ClickOnceSignerTests : IDisposable
private readonly DirectoryService _directoryService = new(Mock.Of>());
private readonly ClickOnceSigner _signer;
+ private const string DeploymentManifestValidContent = @"
+
+
+
+
+
+
+
+
+
+
+
+
+
+";
+
public ClickOnceSignerTests()
{
_signer = new ClickOnceSigner(
@@ -215,7 +231,7 @@ public async Task SignAsync_WhenFilesIsClickOnceFile_Signs(string publisherName)
FileInfo applicationFile = AddFile(
containerSpy,
temporaryDirectory.Directory,
- string.Empty,
+ DeploymentManifestValidContent,
"MyApp.application");
FileInfo dllDeployFile = AddFile(
containerSpy,
@@ -364,7 +380,7 @@ public async Task SignAsync_WhenFilesIsClickOnceFileWithoutContent_Signs()
FileInfo applicationFile = AddFile(
containerSpy,
temporaryDirectory.Directory,
- string.Empty,
+ DeploymentManifestValidContent,
"MyApp.application");
SignOptions options = new(
@@ -547,6 +563,147 @@ public void CopySigningDependencies_CopiesCorrectFiles()
}
}
+ [Fact]
+ public async Task SignAsync_WhenFilesIsClickOnce_DetectsCorrectManifest()
+ {
+ const string commonName = "Test certificate (DO NOT TRUST)";
+
+ using (TemporaryDirectory temporaryDirectory = new(_directoryService))
+ {
+ FileInfo clickOnceFile = new(
+ Path.Combine(
+ temporaryDirectory.Directory.FullName,
+ $"{Path.GetRandomFileName()}.clickonce"));
+
+ ContainerSpy containerSpy = new(clickOnceFile);
+
+ FileInfo applicationFile = AddFile(
+ containerSpy,
+ temporaryDirectory.Directory,
+ DeploymentManifestValidContent,
+ "MyApp.application");
+ // This is an incomplete manifest --- just enough to satisfy SignAsync(...)'s requirements.
+ FileInfo manifestFile = AddFile(
+ containerSpy,
+ temporaryDirectory.Directory,
+ @$"
+
+
+",
+ "MyApp_1_0_0_0", "MyApp.dll.manifest");
+ // A second, unrelated manifest file, which we want to ignore and not sign.
+ FileInfo secondManifestFile = AddFile(
+ containerSpy,
+ temporaryDirectory.Directory,
+ @$"
+
+
+",
+ "MyApp_1_0_0_0", "Some.Dependency.dll.manifest");
+
+
+ SignOptions options = new(
+ "ApplicationName",
+ "PublisherName",
+ "Description",
+ new Uri("https://description.test"),
+ HashAlgorithmName.SHA256,
+ HashAlgorithmName.SHA256,
+ new Uri("http://timestamp.test"),
+ matcher: null,
+ antiMatcher: null);
+
+ using (X509Certificate2 certificate = CreateCertificate())
+ using (RSA privateKey = certificate.GetRSAPrivateKey()!)
+ {
+ Mock signatureAlgorithmProvider = new();
+ Mock certificateProvider = new();
+
+ certificateProvider.Setup(x => x.GetCertificateAsync(It.IsAny()))
+ .ReturnsAsync(certificate);
+
+ signatureAlgorithmProvider.Setup(x => x.GetRsaAsync(It.IsAny()))
+ .ReturnsAsync(privateKey);
+
+ Mock serviceProvider = new();
+ AggregatingSignerSpy aggregatingSignerSpy = new();
+
+ serviceProvider.Setup(x => x.GetService(It.IsAny()))
+ .Returns(aggregatingSignerSpy);
+
+ Mock mageCli = new();
+ string expectedArgs = $"-update \"{manifestFile.FullName}\" -a sha256RSA -n \"{options.ApplicationName}\"";
+ mageCli.Setup(x => x.RunAsync(
+ It.Is(args => string.Equals(expectedArgs, args, StringComparison.Ordinal))))
+ .ReturnsAsync(0);
+
+ string publisher;
+
+ if (string.IsNullOrEmpty(options.PublisherName))
+ {
+ publisher = certificate.SubjectName.Name;
+ }
+ else
+ {
+ publisher = options.PublisherName;
+ }
+
+ expectedArgs = $"-update \"{applicationFile.FullName}\" -a sha256RSA -n \"{options.ApplicationName}\" -pub \"{publisher}\" -appm \"{manifestFile.FullName}\" -SupportURL https://description.test/";
+ mageCli.Setup(x => x.RunAsync(
+ It.Is(args => string.Equals(expectedArgs, args, StringComparison.Ordinal))))
+ .ReturnsAsync(0);
+
+ Mock manifestSigner = new();
+ Mock fileMatcher = new();
+
+ manifestSigner.Setup(
+ x => x.Sign(
+ It.Is(fi => fi.Name == manifestFile.Name),
+ It.Is(c => ReferenceEquals(certificate, c)),
+ It.Is(rsa => ReferenceEquals(privateKey, rsa)),
+ It.Is(o => ReferenceEquals(options, o))));
+
+ manifestSigner.Setup(
+ x => x.Sign(
+ It.Is(fi => fi.Name == applicationFile.Name),
+ It.Is(c => ReferenceEquals(certificate, c)),
+ It.Is(rsa => ReferenceEquals(privateKey, rsa)),
+ It.Is(o => ReferenceEquals(options, o))));
+
+ ILogger logger = Mock.Of>();
+ ClickOnceSigner signer = new(
+ signatureAlgorithmProvider.Object,
+ certificateProvider.Object,
+ serviceProvider.Object,
+ mageCli.Object,
+ manifestSigner.Object,
+ logger,
+ fileMatcher.Object);
+
+ await signer.SignAsync(new[] { applicationFile }, options);
+
+ // Verify that files have been renamed back.
+ foreach (FileInfo file in containerSpy.Files)
+ {
+ file.Refresh();
+
+ Assert.True(file.Exists);
+ }
+
+ mageCli.VerifyAll();
+ manifestSigner.VerifyAll();
+
+ // make sure we never tried to sign the second manifest file
+ manifestSigner.Verify(
+ x => x.Sign(
+ It.Is(fi => fi.Name == secondManifestFile.Name),
+ It.Is(c => ReferenceEquals(certificate, c)),
+ It.Is(rsa => ReferenceEquals(privateKey, rsa)),
+ It.Is(o => ReferenceEquals(options, o))), Times.Never());
+ }
+ }
+ }
+
private static FileInfo AddFile(
ContainerSpy containerSpy,
DirectoryInfo directory,
@@ -578,4 +735,4 @@ private static X509Certificate2 CreateCertificate()
return request.CreateSelfSigned(now.AddMinutes(-5), now.AddMinutes(5));
}
}
-}
\ No newline at end of file
+}