diff --git a/deployment/cake/apps-wpf-tasks.cake b/deployment/cake/apps-wpf-tasks.cake index e5e28f4..3538401 100644 --- a/deployment/cake/apps-wpf-tasks.cake +++ b/deployment/cake/apps-wpf-tasks.cake @@ -155,26 +155,11 @@ public class WpfProcessor : ProcessorBase CakeContext.DeleteFiles(filesToDelete); } - // We know we *highly likely* need to sign, so try doing this upfront - if (!string.IsNullOrWhiteSpace(BuildContext.General.CodeSign.CertificateSubjectName)) + if (BuildContext.General.CodeSign.IsAvailable || + BuildContext.General.AzureCodeSign.IsAvailable) { - BuildContext.CakeContext.Information("Searching for packagable files to sign:"); - - var projectFilesToSign = new List(); - - var exeSignFilesSearchPattern = $"{BuildContext.General.OutputRootDirectory}/{wpfApp}/**/*.exe"; - BuildContext.CakeContext.Information($" - {exeSignFilesSearchPattern}"); - projectFilesToSign.AddRange(BuildContext.CakeContext.GetFiles(exeSignFilesSearchPattern)); - - var dllSignFilesSearchPattern = $"{BuildContext.General.OutputRootDirectory}/{wpfApp}/**/*.dll"; - BuildContext.CakeContext.Information($" - {dllSignFilesSearchPattern}"); - projectFilesToSign.AddRange(BuildContext.CakeContext.GetFiles(dllSignFilesSearchPattern)); - - var signToolCommand = string.Format("sign /a /t {0} /n {1} /fd {2}", BuildContext.General.CodeSign.TimeStampUri, - BuildContext.General.CodeSign.CertificateSubjectName, BuildContext.General.CodeSign.HashAlgorithm); - - SignFiles(BuildContext, signToolCommand, projectFilesToSign); - } + SignFilesInDirectory(BuildContext, outputDirectory, string.Empty); + } else { BuildContext.CakeContext.Warning("No signing certificate subject name provided, not signing any files"); diff --git a/deployment/cake/components-tasks.cake b/deployment/cake/components-tasks.cake index ca91e6f..1c5bef2 100644 --- a/deployment/cake/components-tasks.cake +++ b/deployment/cake/components-tasks.cake @@ -314,27 +314,7 @@ public class ComponentsProcessor : ProcessorBase BuildContext.CakeContext.LogSeparator(); } - var codeSign = (!BuildContext.General.IsCiBuild && - !BuildContext.General.IsLocalBuild && - !string.IsNullOrWhiteSpace(BuildContext.General.CodeSign.CertificateSubjectName)); - if (codeSign) - { - // For details, see https://docs.microsoft.com/en-us/nuget/create-packages/sign-a-package - // nuget sign MyPackage.nupkg -CertificateSubjectName -Timestamper - var filesToSign = CakeContext.GetFiles($"{BuildContext.General.OutputRootDirectory}/*.nupkg"); - - foreach (var fileToSign in filesToSign) - { - CakeContext.Information($"Signing NuGet package '{fileToSign}' using certificate subject '{BuildContext.General.CodeSign.CertificateSubjectName}'"); - - var exitCode = CakeContext.StartProcess(BuildContext.General.NuGet.Executable, new ProcessSettings - { - Arguments = $"sign \"{fileToSign}\" -CertificateSubjectName \"{BuildContext.General.CodeSign.CertificateSubjectName}\" -Timestamper \"{BuildContext.General.CodeSign.TimeStampUri}\"" - }); - - CakeContext.Information("Signing NuGet package exited with '{0}'", exitCode); - } - } + await SignNuGetPackageAsync(); } public override async Task DeployAsync() @@ -378,4 +358,22 @@ public class ComponentsProcessor : ProcessorBase { } + + private async Task SignNuGetPackageAsync() + { + if (BuildContext.General.IsCiBuild || + BuildContext.General.IsLocalBuild) + { + return; + } + + // For details, see https://docs.microsoft.com/en-us/nuget/create-packages/sign-a-package + // nuget sign MyPackage.nupkg -CertificateSubjectName -Timestamper + var filesToSign = CakeContext.GetFiles($"{BuildContext.General.OutputRootDirectory}/*.nupkg"); + + foreach (var fileToSign in filesToSign) + { + SignNuGetPackage(BuildContext, fileToSign.FullPath); + } + } } \ No newline at end of file diff --git a/deployment/cake/generic-tasks.cake b/deployment/cake/generic-tasks.cake index 41c7139..af67e7d 100644 --- a/deployment/cake/generic-tasks.cake +++ b/deployment/cake/generic-tasks.cake @@ -276,10 +276,10 @@ Task("CodeSign") return; } - var certificateSubjectName = buildContext.General.CodeSign.CertificateSubjectName; - if (string.IsNullOrWhiteSpace(certificateSubjectName)) + if (!buildContext.General.CodeSign.IsAvailable && + !buildContext.General.AzureCodeSign.IsAvailable) { - Information("Skipping code signing because the certificate subject name was not specified"); + Information("Skipping code signing since no option is available"); return; } diff --git a/deployment/cake/generic-variables.cake b/deployment/cake/generic-variables.cake index 246657f..f9f3e76 100644 --- a/deployment/cake/generic-variables.cake +++ b/deployment/cake/generic-variables.cake @@ -39,6 +39,7 @@ public class GeneralContext : BuildContextWithItemsBase public SolutionContext Solution { get; set; } public SourceLinkContext SourceLink { get; set; } public CodeSignContext CodeSign { get; set; } + public AzureCodeSignContext AzureCodeSign { get; set; } public RepositoryContext Repository { get; set; } public SonarQubeContext SonarQube { get; set; } @@ -338,6 +339,19 @@ public class CodeSignContext : BuildContextBase public string TimeStampUri { get; set; } public string HashAlgorithm { get; set; } + public bool IsAvailable + { + get + { + if (string.IsNullOrWhiteSpace(CertificateSubjectName)) + { + return false; + } + + return true; + } + } + protected override void ValidateContext() { @@ -345,7 +359,7 @@ public class CodeSignContext : BuildContextBase protected override void LogStateInfoForContext() { - if (string.IsNullOrWhiteSpace(CertificateSubjectName)) + if (!IsAvailable) { CakeContext.Information($"Code signing is not configured"); return; @@ -359,6 +373,62 @@ public class CodeSignContext : BuildContextBase //------------------------------------------------------------- +public class AzureCodeSignContext : BuildContextBase +{ + public AzureCodeSignContext(IBuildContext parentBuildContext) + : base(parentBuildContext) + { + } + + public string VaultName { get; set; } + public string VaultUrl { get { return $"https://{VaultName}.vault.azure.net"; } } + public string CertificateName { get; set; } + public string TimeStampUri { get; set; } + public string HashAlgorithm { get; set; } + public string TenantId { get; set; } + public string ClientId { get; set; } + public string ClientSecret { get; set; } + + public bool IsAvailable + { + get + { + if (string.IsNullOrWhiteSpace(VaultName) || + string.IsNullOrWhiteSpace(CertificateName) || + string.IsNullOrWhiteSpace(TenantId) || + string.IsNullOrWhiteSpace(ClientId) || + string.IsNullOrWhiteSpace(ClientSecret)) + { + return false; + } + + return true; + } + } + + protected override void ValidateContext() + { + + } + + protected override void LogStateInfoForContext() + { + if (!IsAvailable) + { + CakeContext.Information($"Azure Code signing is not configured"); + return; + } + + CakeContext.Information($"Azure Code vault name: '{VaultName}'"); + CakeContext.Information($"Azure Code vault URL: '{VaultUrl}'"); + CakeContext.Information($"Azure Code signing certificate name: '{CertificateName}'"); + CakeContext.Information($"Azure Code signing timestamp uri: '{TimeStampUri}'"); + CakeContext.Information($"Azure Code signing hash algorithm: '{HashAlgorithm}'"); + } +} + +//------------------------------------------------------------- + public class RepositoryContext : BuildContextBase { public RepositoryContext(IBuildContext parentBuildContext) @@ -498,6 +568,17 @@ private GeneralContext InitializeGeneralContext(BuildContext buildContext, IBuil HashAlgorithm = buildContext.BuildServer.GetVariable("CodeSignHashAlgorithm", "SHA256", showValue: true) }; + data.AzureCodeSign = new AzureCodeSignContext(data) + { + VaultName = buildContext.BuildServer.GetVariable("AzureCodeSignVaultName", showValue: true), + CertificateName = buildContext.BuildServer.GetVariable("AzureCodeSignCertificateName", showValue: true), + TimeStampUri = buildContext.BuildServer.GetVariable("AzureCodeSignTimeStampUri", "http://timestamp.digicert.com", showValue: true), + HashAlgorithm = buildContext.BuildServer.GetVariable("AzureCodeSignHashAlgorithm", "SHA256", showValue: true), + TenantId = buildContext.BuildServer.GetVariable("AzureCodeSignTenantId", showValue: false), + ClientId = buildContext.BuildServer.GetVariable("AzureCodeSignClientId", showValue: false), + ClientSecret = buildContext.BuildServer.GetVariable("AzureCodeSignClientSecret", showValue: false), + }; + data.Repository = new RepositoryContext(data) { Url = buildContext.BuildServer.GetVariable("RepositoryUrl", showValue: true), diff --git a/deployment/cake/installers-innosetup.cake b/deployment/cake/installers-innosetup.cake index c3992be..cfad26a 100644 --- a/deployment/cake/installers-innosetup.cake +++ b/deployment/cake/installers-innosetup.cake @@ -73,33 +73,64 @@ public class InnoSetupInstaller : IInstaller fileContents = fileContents.Replace("[VERSION_DISPLAY]", BuildContext.General.Version.FullSemVer); fileContents = fileContents.Replace("[WIZARDIMAGEFILE]", string.Format("logo_large{0}", setupSuffix)); - var signTool = string.Empty; - if (!string.IsNullOrWhiteSpace(BuildContext.General.CodeSign.CertificateSubjectName)) + var signToolIndex = GetRandomSignToolIndex(); + + try { - signTool = string.Format("SignTool={0}", BuildContext.General.CodeSign.CertificateSubjectName); - } + var codeSignContext = BuildContext.General.CodeSign; + var azureCodeSignContext = BuildContext.General.AzureCodeSign; + + var signTool = string.Empty; - fileContents = fileContents.Replace("[SIGNTOOL]", signTool); - System.IO.File.WriteAllText(innoSetupScriptFileName, fileContents); + var signToolFileName = GetSignToolFileName(BuildContext); + if (!string.IsNullOrWhiteSpace(signToolFileName)) + { + var signToolName = DateTime.Now.ToString("yyyyMMddHHmmss"); + var signToolCommandLine = GetSignToolCommandLine(BuildContext); - BuildContext.CakeContext.Information("Generating Inno Setup packages, this can take a while, especially when signing is enabled..."); + BuildContext.CakeContext.Information("Adding random sign tool config for Inno Setup"); - BuildContext.CakeContext.InnoSetup(innoSetupScriptFileName, new InnoSetupSettings - { - OutputDirectory = innoSetupReleasesRoot - }); + using (var registryKey = Microsoft.Win32.Registry.CurrentUser.OpenSubKey(GetRegistryKey(), true)) + { + var registryValueName = GetSignToolIndexName(signToolIndex); - if (BuildContext.Wpf.UpdateDeploymentsShare) - { - BuildContext.CakeContext.Information("Copying Inno Setup files to deployments share at '{0}'", installersOnDeploymentsShare); + // Important: must end with "$f" + var signToolRegistryValue = $"{signToolName}=\"{signToolFileName}\" {signToolCommandLine} \"$f\""; - // Copy the following files: - // - Setup.exe => [projectName]-[version].exe - // - Setup.exe => [projectName]-[channel].exe + registryKey.SetValue(registryValueName, signToolRegistryValue); + } + + signTool = string.Format("SignTool={0}", signToolName); + } + + fileContents = fileContents.Replace("[SIGNTOOL]", signTool); + System.IO.File.WriteAllText(innoSetupScriptFileName, fileContents); + + BuildContext.CakeContext.Information("Generating Inno Setup packages, this can take a while, especially when signing is enabled..."); + + BuildContext.CakeContext.InnoSetup(innoSetupScriptFileName, new InnoSetupSettings + { + OutputDirectory = innoSetupReleasesRoot + }); - var installerSourceFile = System.IO.Path.Combine(innoSetupReleasesRoot, $"{projectName}_{BuildContext.General.Version.FullSemVer}.exe"); - BuildContext.CakeContext.CopyFile(installerSourceFile, System.IO.Path.Combine(installersOnDeploymentsShare, $"{projectName}_{BuildContext.General.Version.FullSemVer}.exe")); - BuildContext.CakeContext.CopyFile(installerSourceFile, System.IO.Path.Combine(installersOnDeploymentsShare, $"{projectName}{setupSuffix}.exe")); + if (BuildContext.Wpf.UpdateDeploymentsShare) + { + BuildContext.CakeContext.Information("Copying Inno Setup files to deployments share at '{0}'", installersOnDeploymentsShare); + + // Copy the following files: + // - Setup.exe => [projectName]-[version].exe + // - Setup.exe => [projectName]-[channel].exe + + var installerSourceFile = System.IO.Path.Combine(innoSetupReleasesRoot, $"{projectName}_{BuildContext.General.Version.FullSemVer}.exe"); + BuildContext.CakeContext.CopyFile(installerSourceFile, System.IO.Path.Combine(installersOnDeploymentsShare, $"{projectName}_{BuildContext.General.Version.FullSemVer}.exe")); + BuildContext.CakeContext.CopyFile(installerSourceFile, System.IO.Path.Combine(installersOnDeploymentsShare, $"{projectName}{setupSuffix}.exe")); + } + } + finally + { + BuildContext.CakeContext.Information("Removing random sign tool config for Inno Setup"); + + RemoveSignToolFromRegistry(signToolIndex); } } @@ -222,4 +253,53 @@ public class InnoSetupInstaller : IInstaller return installersOnDeploymentsShare; } + + //------------------------------------------------------------- + + private string GetRegistryKey() + { + return "Software\\Jordan Russell\\Inno Setup\\SignTools"; + } + + //------------------------------------------------------------- + + private int GetRandomSignToolIndex() + { + using (var registryKey = Microsoft.Win32.Registry.CurrentUser.CreateSubKey(GetRegistryKey())) + { + for (int i = 0; i < 100; i++) + { + var valueName = GetSignToolIndexName(i); + + if (registryKey.GetValue(valueName) is null) + { + // Immediately lock it + registryKey.SetValue(valueName, "reserved"); + + return i; + } + } + } + + throw new Exception("Could not find any empty slots for the sign tool, please clean up the sign tool registry for Inno Setup"); + } + + //------------------------------------------------------------- + + private string GetSignToolIndexName(int index) + { + return $"SignTool{index}"; + } + + //------------------------------------------------------------- + + private void RemoveSignToolFromRegistry(int index) + { + using (var registryKey = Microsoft.Win32.Registry.CurrentUser.CreateSubKey(GetRegistryKey())) + { + var valueName = GetSignToolIndexName(index); + + registryKey.DeleteValue(valueName, false); + } + } } diff --git a/deployment/cake/installers-msix.cake b/deployment/cake/installers-msix.cake index 6e71d63..78096ca 100644 --- a/deployment/cake/installers-msix.cake +++ b/deployment/cake/installers-msix.cake @@ -51,17 +51,6 @@ public class MsixInstaller : IInstaller return; } - var signToolCommand = string.Empty; - if (!string.IsNullOrWhiteSpace(BuildContext.General.CodeSign.CertificateSubjectName)) - { - signToolCommand = string.Format("sign /a /t {0} /n {1} /fd {2}", BuildContext.General.CodeSign.TimeStampUri, - BuildContext.General.CodeSign.CertificateSubjectName, BuildContext.General.CodeSign.HashAlgorithm); - } - else - { - BuildContext.CakeContext.Warning("No sign tool is defined, MSIX will not be installable to (most or all) users"); - } - BuildContext.CakeContext.LogSeparator($"Packaging app '{projectName}' using MSIX"); var deploymentShare = BuildContext.Wpf.GetDeploymentShareForProject(projectName); @@ -119,15 +108,16 @@ public class MsixInstaller : IInstaller BuildContext.CakeContext.CopyFiles(appSourceDirectory, appTargetDirectory, true); - BuildContext.CakeContext.Information($"Signing files in '{appTargetDirectory}'"); - - var filesToSign = new List(); - - filesToSign.AddRange(BuildContext.CakeContext.GetFiles($"{appTargetDirectory}/**/*.dll").Select(x => x.FullPath)); - filesToSign.AddRange(BuildContext.CakeContext.GetFiles($"{appTargetDirectory}/**/*.exe").Select(x => x.FullPath)); + if (BuildContext.General.CodeSign.IsAvailable || + BuildContext.General.AzureCodeSign.IsAvailable) + { + SignFilesInDirectory(BuildContext, appTargetDirectory, string.Empty); + } + else + { + BuildContext.CakeContext.Warning("No sign tool is defined, MSIX will not be installable to (most or all) users"); + } - SignFiles(BuildContext, signToolCommand, filesToSign); - BuildContext.CakeContext.Information("Generating MSIX packages using MakeAppX..."); var processSettings = new ProcessSettings @@ -154,7 +144,7 @@ public class MsixInstaller : IInstaller } } - SignFile(BuildContext, signToolCommand, installerSourceFile); + SignFile(BuildContext, installerSourceFile); // Always copy the AppInstaller if available if (BuildContext.CakeContext.FileExists(msixUpdateScriptFileName)) diff --git a/deployment/cake/lib-signing.cake b/deployment/cake/lib-signing.cake index 97dfc73..0c3bef6 100644 --- a/deployment/cake/lib-signing.cake +++ b/deployment/cake/lib-signing.cake @@ -1,4 +1,8 @@ +#tool "dotnet:?package=AzureSignTool&version=6.0.0" +#tool "dotnet:?package=NuGetKeyVaultSignTool&version=3.2.3" + private static string _signToolFileName; +private static string _azureSignToolFileName; //------------------------------------------------------------- @@ -25,43 +29,80 @@ public static bool ShouldSignImmediately(BuildContext buildContext, string proje public static void SignProjectFiles(BuildContext buildContext, string projectName) { - var certificateSubjectName = buildContext.General.CodeSign.CertificateSubjectName; - if (string.IsNullOrWhiteSpace(certificateSubjectName)) - { - buildContext.CakeContext.Information("Skipping code signing because the certificate subject name was not specified"); - return; - } + var outputDirectory = string.Format("{0}/{1}", buildContext.General.OutputRootDirectory, projectName); - var codeSignWildCard = buildContext.General.CodeSign.WildCard; + var codeSignContext = buildContext.General.CodeSign; + var codeSignWildCard = codeSignContext.WildCard; if (string.IsNullOrWhiteSpace(codeSignWildCard)) { // Empty, we need to override with project name for valid default value codeSignWildCard = projectName; } - var outputDirectory = string.Format("{0}/{1}", buildContext.General.OutputRootDirectory, projectName); + SignFilesInDirectory(buildContext, outputDirectory, codeSignWildCard); +} + +//------------------------------------------------------------- + +public static void SignFilesInDirectory(BuildContext buildContext, string directory, string codeSignWildCard) +{ + var codeSignContext = buildContext.General.CodeSign; + var azureCodeSignContext = buildContext.General.AzureCodeSign; + + var certificateSubjectName = buildContext.General.CodeSign.CertificateSubjectName; + if (!codeSignContext.IsAvailable && + !azureCodeSignContext.IsAvailable) + { + buildContext.CakeContext.Information("Skipping code signing because none of the options is available"); + return; + } var projectFilesToSign = new List(); - var exeSignFilesSearchPattern = string.Format("{0}/**/*{1}*.exe", outputDirectory, codeSignWildCard); + if (!string.IsNullOrWhiteSpace(codeSignWildCard)) + { + // Make sure the pattern becomes *[wildcard]* + codeSignWildCard += "*"; + } + else + { + codeSignWildCard = string.Empty; + } + + var exeSignFilesSearchPattern = string.Format("{0}/**/*{1}.exe", directory, codeSignWildCard); buildContext.CakeContext.Information(exeSignFilesSearchPattern); projectFilesToSign.AddRange(buildContext.CakeContext.GetFiles(exeSignFilesSearchPattern)); - var dllSignFilesSearchPattern = string.Format("{0}/**/*{1}*.dll", outputDirectory, codeSignWildCard); + var dllSignFilesSearchPattern = string.Format("{0}/**/*{1}.dll", directory, codeSignWildCard); buildContext.CakeContext.Information(dllSignFilesSearchPattern); projectFilesToSign.AddRange(buildContext.CakeContext.GetFiles(dllSignFilesSearchPattern)); - buildContext.CakeContext.Information("Found '{0}' files to code sign for '{1}'", projectFilesToSign.Count, projectName); + buildContext.CakeContext.Information("Found '{0}' files to code sign", projectFilesToSign.Count); - var signToolCommand = string.Format("sign /a /t {0} /n {1} /fd {2}", buildContext.General.CodeSign.TimeStampUri, - certificateSubjectName, buildContext.General.CodeSign.HashAlgorithm); + var signToolCommand = GetSignToolCommandLine(buildContext); - SignFiles(buildContext, signToolCommand, projectFilesToSign); + SignFiles(buildContext, signToolCommand, projectFilesToSign, null); } //------------------------------------------------------------- -public static void SignFiles(BuildContext buildContext, string signToolCommand, IEnumerable fileNames, string additionalCommandLineArguments = null) +public static void SignFile(BuildContext buildContext, FilePath filePath) +{ + SignFile(buildContext, filePath.FullPath); +} + +//------------------------------------------------------------- + +public static void SignFile(BuildContext buildContext, string fileName) +{ + var signToolCommand = GetSignToolCommandLine(buildContext); + + SignFiles(buildContext, signToolCommand, new [] { fileName }, null); +} + +//------------------------------------------------------------- + +public static void SignFiles(BuildContext buildContext, string signToolCommand, IEnumerable fileNames, string additionalCommandLineArguments) { if (fileNames.Any()) { @@ -76,7 +117,7 @@ public static void SignFiles(BuildContext buildContext, string signToolCommand, //------------------------------------------------------------- -public static void SignFiles(BuildContext buildContext, string signToolCommand, IEnumerable fileNames, string additionalCommandLineArguments = null) +public static void SignFiles(BuildContext buildContext, string signToolCommand, IEnumerable fileNames, string additionalCommandLineArguments) { if (fileNames.Any()) { @@ -91,22 +132,46 @@ public static void SignFiles(BuildContext buildContext, string signToolCommand, //------------------------------------------------------------- -public static void SignFile(BuildContext buildContext, string signToolCommand, string fileName, string additionalCommandLineArguments = null) +public static void SignFile(BuildContext buildContext, string signToolCommand, string fileName, string additionalCommandLineArguments) { - // Skip code signing in specific scenarios - if (string.IsNullOrWhiteSpace(signToolCommand)) + var codeSignContext = buildContext.General.CodeSign; + var azureCodeSignContext = buildContext.General.AzureCodeSign; + + if (string.IsNullOrWhiteSpace(_signToolFileName)) { - return; + // Always fetch, it is used for verification + _signToolFileName = FindWindowsSignToolFileName(buildContext); } - if (string.IsNullOrWhiteSpace(_signToolFileName)) + if (string.IsNullOrWhiteSpace(_azureSignToolFileName)) { - _signToolFileName = FindSignToolFileName(buildContext); + _azureSignToolFileName = FindAzureSignToolFileName(buildContext); } - if (string.IsNullOrWhiteSpace(_signToolFileName)) + var signToolFileName = _signToolFileName; + + // Azure always wins + if (azureCodeSignContext.IsAvailable) + { + signToolFileName = _azureSignToolFileName; + } + + SignFile(buildContext, signToolFileName, signToolCommand, fileName, additionalCommandLineArguments); +} + +//------------------------------------------------------------- + +public static void SignFile(BuildContext buildContext, string signToolFileName, string signToolCommand, string fileName, string additionalCommandLineArguments) +{ + // Skip code signing in specific scenarios + if (string.IsNullOrWhiteSpace(signToolCommand)) { - throw new InvalidOperationException("Cannot find signtool.exe, make sure to install a Windows Development Kit"); + return; + } + + if (string.IsNullOrWhiteSpace(signToolFileName)) + { + throw new InvalidOperationException("Cannot find signtool, make sure to install a Windows Development Kit"); } buildContext.CakeContext.Information(string.Empty); @@ -127,6 +192,8 @@ public static void SignFile(BuildContext buildContext, string signToolCommand, s RedirectStandardOutput = true }; + // Note: we can safely use SignTool.exe here + using (var checkProcess = buildContext.CakeContext.StartAndReturnProcess(_signToolFileName, checkProcessSettings)) { checkProcess.WaitForExit(); @@ -156,7 +223,7 @@ public static void SignFile(BuildContext buildContext, string signToolCommand, s Silent = true }; - using (var signProcess = buildContext.CakeContext.StartAndReturnProcess(_signToolFileName, signProcessSettings)) + using (var signProcess = buildContext.CakeContext.StartAndReturnProcess(signToolFileName, signProcessSettings)) { signProcess.WaitForExit(); @@ -181,7 +248,58 @@ public static void SignFile(BuildContext buildContext, string signToolCommand, s //------------------------------------------------------------- -public static string FindSignToolFileName(BuildContext buildContext) +public static void SignNuGetPackage(BuildContext buildContext, string fileName) +{ + var codeSignContext = buildContext.General.CodeSign; + var azureCodeSignContext = buildContext.General.AzureCodeSign; + + if (buildContext.General.IsCiBuild || + buildContext.General.IsLocalBuild) + { + return; + } + + buildContext.CakeContext.Information($"Signing NuGet package '{fileName}'"); + + if (azureCodeSignContext.IsAvailable) + { + var signToolFileName = FindNuGetAzureSignToolFileName(buildContext); + var signToolCommandLine = string.Format("sign -kvu {0} -kvt {1} -kvi {2} -kvs {3} -kvc {4} -tr {5} -fd {6}", + azureCodeSignContext.VaultUrl, + azureCodeSignContext.TenantId, + azureCodeSignContext.ClientId, + azureCodeSignContext.ClientSecret, + azureCodeSignContext.CertificateName, + azureCodeSignContext.TimeStampUri, + azureCodeSignContext.HashAlgorithm); + + var finalCommand = $"{signToolFileName} {signToolCommandLine} {fileName}"; + + buildContext.CakeContext.Information($"{finalCommand}'"); + + SignFile(buildContext, signToolFileName, signToolCommandLine, fileName, null); + + return; + } + + if (codeSignContext.IsAvailable) + { + var exitCode = buildContext.CakeContext.StartProcess(buildContext.General.NuGet.Executable, new ProcessSettings + { + Arguments = $"sign \"{fileName}\" -CertificateSubjectName \"{codeSignContext.CertificateSubjectName}\" -Timestamper \"{codeSignContext.TimeStampUri}\"" + }); + + buildContext.CakeContext.Information("Signing NuGet package exited with '{0}'", exitCode); + + return; + } + + throw new NotSupportedException("No supported code signing method could be found"); +} + +//------------------------------------------------------------- + +public static string FindWindowsSignToolFileName(BuildContext buildContext) { var directory = FindLatestWindowsKitsDirectory(buildContext); if (directory != null) @@ -191,3 +309,79 @@ public static string FindSignToolFileName(BuildContext buildContext) return null; } + +//------------------------------------------------------------- + +public static string FindAzureSignToolFileName(BuildContext buildContext) +{ + var path = buildContext.CakeContext.Tools.Resolve("AzureSignTool.exe"); + + buildContext.CakeContext.Information("Found path '{0}'", path); + + return path.FullPath; +} + +//------------------------------------------------------------- + +public static string FindNuGetAzureSignToolFileName(BuildContext buildContext) +{ + var path = buildContext.CakeContext.Tools.Resolve("NuGetKeyVaultSignTool.exe"); + + buildContext.CakeContext.Information("Found path '{0}'", path); + + return path.FullPath; +} + +//------------------------------------------------------------- + +public static string GetSignToolFileName(BuildContext buildContext) +{ + var codeSignContext = buildContext.General.CodeSign; + var azureCodeSignContext = buildContext.General.AzureCodeSign; + + // Azure first + if (azureCodeSignContext.IsAvailable) + { + return FindAzureSignToolFileName(buildContext); + } + + if (codeSignContext.IsAvailable) + { + return FindWindowsSignToolFileName(buildContext); + } + + return string.Empty; +} + +//------------------------------------------------------------- + +public static string GetSignToolCommandLine(BuildContext buildContext) +{ + var codeSignContext = buildContext.General.CodeSign; + var azureCodeSignContext = buildContext.General.AzureCodeSign; + + var signToolCommand = string.Empty; + + if (codeSignContext.IsAvailable) + { + signToolCommand = string.Format("sign /a /t {0} /n {1} /fd {2}", + codeSignContext.TimeStampUri, + codeSignContext.CertificateSubjectName, + codeSignContext.HashAlgorithm); + } + + // Note: Azure always wins + if (azureCodeSignContext.IsAvailable) + { + signToolCommand = string.Format("sign -kvu {0} -kvt {1} -kvi {2} -kvs {3} -kvc {4} -tr {5} -fd {6}", + azureCodeSignContext.VaultUrl, + azureCodeSignContext.TenantId, + azureCodeSignContext.ClientId, + azureCodeSignContext.ClientSecret, + azureCodeSignContext.CertificateName, + azureCodeSignContext.TimeStampUri, + azureCodeSignContext.HashAlgorithm); + } + + return signToolCommand; +} diff --git a/deployment/cake/tools-tasks.cake b/deployment/cake/tools-tasks.cake index 5ad3206..7229ab1 100644 --- a/deployment/cake/tools-tasks.cake +++ b/deployment/cake/tools-tasks.cake @@ -283,24 +283,10 @@ SHA512 CHECKSUMS GENERATED BY BUILD TOOL: CakeContext.DeleteFiles(objFiles); // We know we *highly likely* need to sign, so try doing this upfront - if (!string.IsNullOrWhiteSpace(BuildContext.General.CodeSign.CertificateSubjectName)) + if (BuildContext.General.CodeSign.IsAvailable || + BuildContext.General.AzureCodeSign.IsAvailable) { - BuildContext.CakeContext.Information("Searching for packagable files to sign:"); - - var projectFilesToSign = new List(); - - var exeSignFilesSearchPattern = $"{BuildContext.General.OutputRootDirectory}/{tool}/**/*.exe"; - BuildContext.CakeContext.Information($" - {exeSignFilesSearchPattern}"); - projectFilesToSign.AddRange(BuildContext.CakeContext.GetFiles(exeSignFilesSearchPattern)); - - var dllSignFilesSearchPattern = $"{BuildContext.General.OutputRootDirectory}/{tool}/**/*.dll"; - BuildContext.CakeContext.Information($" - {dllSignFilesSearchPattern}"); - projectFilesToSign.AddRange(BuildContext.CakeContext.GetFiles(dllSignFilesSearchPattern)); - - var signToolCommand = string.Format("sign /a /t {0} /n {1} /fd {2}", BuildContext.General.CodeSign.TimeStampUri, - BuildContext.General.CodeSign.CertificateSubjectName, BuildContext.General.CodeSign.HashAlgorithm); - - SignFiles(BuildContext, signToolCommand, projectFilesToSign); + SignFilesInDirectory(BuildContext, outputDirectory, string.Empty); } else { @@ -372,28 +358,8 @@ SHA512 CHECKSUMS GENERATED BY BUILD TOOL: BuildContext.CakeContext.LogSeparator(); } - - var codeSign = (!BuildContext.General.IsCiBuild && - !BuildContext.General.IsLocalBuild && - !string.IsNullOrWhiteSpace(BuildContext.General.CodeSign.CertificateSubjectName)); - if (codeSign) - { - // For details, see https://docs.microsoft.com/en-us/nuget/create-packages/sign-a-package - // nuget sign MyPackage.nupkg -CertificateSubjectName -Timestamper - var filesToSign = CakeContext.GetFiles($"{BuildContext.General.OutputRootDirectory}/*.nupkg"); - - foreach (var fileToSign in filesToSign) - { - CakeContext.Information($"Signing NuGet package '{fileToSign}' using certificate subject '{BuildContext.General.CodeSign.CertificateSubjectName}'"); - - var exitCode = CakeContext.StartProcess(BuildContext.General.NuGet.Executable, new ProcessSettings - { - Arguments = $"sign \"{fileToSign}\" -CertificateSubjectName \"{BuildContext.General.CodeSign.CertificateSubjectName}\" -Timestamper \"{BuildContext.General.CodeSign.TimeStampUri}\"" - }); - - CakeContext.Information("Signing NuGet package exited with '{0}'", exitCode); - } - } + + await SignNuGetPackageAsync(); } public override async Task DeployAsync() @@ -446,4 +412,22 @@ SHA512 CHECKSUMS GENERATED BY BUILD TOOL: { } + + private async Task SignNuGetPackageAsync() + { + if (BuildContext.General.IsCiBuild || + BuildContext.General.IsLocalBuild) + { + return; + } + + // For details, see https://docs.microsoft.com/en-us/nuget/create-packages/sign-a-package + // nuget sign MyPackage.nupkg -CertificateSubjectName -Timestamper + var filesToSign = CakeContext.GetFiles($"{BuildContext.General.OutputRootDirectory}/*.nupkg"); + + foreach (var fileToSign in filesToSign) + { + SignNuGetPackage(BuildContext, fileToSign.FullPath); + } + } } \ No newline at end of file diff --git a/src/Directory.Build.analyzers.props b/src/Directory.Build.analyzers.props index 0290b97..d5c5138 100644 --- a/src/Directory.Build.analyzers.props +++ b/src/Directory.Build.analyzers.props @@ -12,11 +12,11 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - + all runtime; build; native; contentfiles; analyzers; buildtransitive - + all runtime; build; native; contentfiles; analyzers; buildtransitive