Skip to content

Commit

Permalink
(#149) Switch to only signing when required
Browse files Browse the repository at this point in the history
We don't want to sign files when we don't need to.  Going forward,
PowerShell scripts are going to be signed when they are committed to
the repository and only re-signed when required.

This commit addresses this need by changing the DAG to use a new
Verify-PowerShellScipts task, rather than the Sign-PowerShellScripts
task.  The latter is still available to call directly, when required,
but only when a valid certificate is in place.

Supporting parameters and build directories have been created, to allow
control over what the tasks due, including the ability to skip the
verification step, using the --shouldVerifyPowerShellScripts command
line argument.

A new verify-powershell.ps1 file has been added to check the list of
incoming files, and the sign-powershell.ps1 has been updated to only
sign when the current signature is invalid.  To aid with getting the
signed files added to back into the repository, the signed files are
uploaded as artifacts of the build.
  • Loading branch information
gep13 committed May 14, 2024
1 parent 043be76 commit 4b9f73b
Show file tree
Hide file tree
Showing 7 changed files with 142 additions and 7 deletions.
4 changes: 2 additions & 2 deletions Chocolatey.Cake.Recipe/Content/build.cake
Original file line number Diff line number Diff line change
Expand Up @@ -570,9 +570,9 @@ public class Builder
BuildParameters.Tasks.CreateChocolateyPackagesTask.IsDependentOn("Configuration-Builder");
BuildParameters.Tasks.CreateChocolateyPackagesTask.IsDependentOn("Obfuscate-Assemblies");
BuildParameters.Tasks.CreateChocolateyPackagesTask.IsDependentOn("Sign-Assemblies");
BuildParameters.Tasks.CreateNuGetPackagesTask.IsDependentOn("Sign-PowerShellScripts");
BuildParameters.Tasks.CreateNuGetPackagesTask.IsDependentOn("Verify-PowerShellScripts");
BuildParameters.Tasks.CreateNuGetPackagesTask.IsDependentOn("Sign-Assemblies");
BuildParameters.Tasks.CreateChocolateyPackagesTask.IsDependentOn("Sign-PowerShellScripts");
BuildParameters.Tasks.CreateChocolateyPackagesTask.IsDependentOn("Verify-PowerShellScripts");
BuildParameters.Tasks.SignMsisTask.IsDependentOn("Sign-Assemblies");
BuildParameters.Tasks.CreateChocolateyPackagesTask.IsDependentOn(prefix + "Build");
BuildParameters.Tasks.ObfuscateAssembliesTask.IsDependeeOf("Sign-Assemblies");
Expand Down
13 changes: 13 additions & 0 deletions Chocolatey.Cake.Recipe/Content/parameters.cake
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,7 @@ public static class BuildParameters
public static Func<List<PSScriptAnalyzerSettings>> GetPSScriptAnalyzerSettings { get; private set; }
public static Func<FilePathCollection> GetMsisToSign { get; private set; }
public static Func<FilePathCollection> GetProjectsToPack { get; private set; }
public static Func<FilePathCollection> GetScriptsToVerify { get; private set; }
public static Func<FilePathCollection> GetScriptsToSign { get; private set; }
public static GitReleaseManagerCredentials GitReleaseManager { get; private set; }
public static string IntegrationTestAssemblyFilePattern { get; private set; }
Expand Down Expand Up @@ -146,6 +147,7 @@ public static class BuildParameters
public static DirectoryPath RootDirectoryPath { get; private set; }
public static bool ShouldAuthenticodeSignMsis { get; private set; }
public static bool ShouldAuthenticodeSignOutputAssemblies { get; private set; }
public static bool ShouldVerifyPowerShellScripts { get; private set; }
public static bool ShouldAuthenticodeSignPowerShellScripts { get; private set; }
public static bool ShouldBuildMsi { get; private set; }
public static bool ShouldBuildNuGetSourcePackage { get; private set; }
Expand Down Expand Up @@ -280,6 +282,7 @@ public static class BuildParameters
context.Information("RestorePackagesDirectory: {0}", RestorePackagesDirectory);
context.Information("ShouldAuthenticodeSignMsis: {0}", BuildParameters.ShouldAuthenticodeSignMsis);
context.Information("ShouldAuthenticodeSignOutputAssemblies: {0}", BuildParameters.ShouldAuthenticodeSignOutputAssemblies);
context.Information("ShouldVerifyPowerShellScripts: {0}", BuildParameters.ShouldVerifyPowerShellScripts);
context.Information("ShouldAuthenticodeSignPowerShellScripts: {0}", BuildParameters.ShouldAuthenticodeSignPowerShellScripts);
context.Information("ShouldBuildMsi: {0}", BuildParameters.ShouldBuildMsi);
context.Information("ShouldBuildNuGetSourcePackage: {0}", BuildParameters.ShouldBuildNuGetSourcePackage);
Expand Down Expand Up @@ -360,6 +363,7 @@ public static class BuildParameters
Func<List<PSScriptAnalyzerSettings>> getPSScriptAnalyzerSettings = null,
Func<FilePathCollection> getMsisToSign = null,
Func<FilePathCollection> getProjectsToPack = null,
Func<FilePathCollection> getScriptsToVerify = null,
Func<FilePathCollection> getScriptsToSign = null,
string integrationTestAssemblyFilePattern = null,
string integrationTestAssemblyProjectPattern = null,
Expand Down Expand Up @@ -391,6 +395,7 @@ public static class BuildParameters
DirectoryPath rootDirectoryPath = null,
bool shouldAuthenticodeSignMsis = true,
bool shouldAuthenticodeSignOutputAssemblies = true,
bool shouldVerifyPowerShellScripts = true,
bool shouldAuthenticodeSignPowerShellScripts = true,
bool shouldBuildMsi = false,
bool shouldBuildNuGetSourcePackage = false,
Expand Down Expand Up @@ -495,6 +500,7 @@ public static class BuildParameters
GetPSScriptAnalyzerSettings = getPSScriptAnalyzerSettings;
GetMsisToSign = getMsisToSign;
GetProjectsToPack = getProjectsToPack;
GetScriptsToVerify = getScriptsToVerify;
GetScriptsToSign = getScriptsToSign;
GitReleaseManager = GetGitReleaseManagerCredentials(context);
IntegrationTestAssemblyFilePattern = integrationTestAssemblyFilePattern ?? "/**/*[tT]ests.[iI]ntegration.dll";
Expand Down Expand Up @@ -542,6 +548,13 @@ public static class BuildParameters
ShouldAuthenticodeSignOutputAssemblies = context.Argument<bool>("shouldAuthenticodeSignOutputAssemblies");
}

ShouldVerifyPowerShellScripts = shouldVerifyPowerShellScripts;

if (context.HasArgument("shouldVerifyPowerShellScripts"))
{
ShouldVerifyPowerShellScripts = context.Argument<bool>("shouldVerifyPowerShellScripts");
}

ShouldAuthenticodeSignPowerShellScripts = shouldAuthenticodeSignPowerShellScripts;

if (context.HasArgument("shouldAuthenticodeSignPowerShellScripts"))
Expand Down
10 changes: 8 additions & 2 deletions Chocolatey.Cake.Recipe/Content/paths.cake
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,8 @@ public class BuildPaths
var chocolateyPackagesOutputDirectory = packagesDirectory + "/Chocolatey";

var dependencyCheckReportsDirectory = buildDirectoryPath + "/DependencyCheckReports";

var signedFilesDirectory = buildDirectoryPath + "/SignedFiles";

// Files
var dotNetFormatOutputFilePath = ((DirectoryPath)testResultsDirectory).CombineWithFilePath("dotnet-format.json");
Expand Down Expand Up @@ -84,7 +86,8 @@ public class BuildPaths
chocolateyPackagesOutputDirectory,
packagesDirectory,
environmentSettingsDirectory,
dependencyCheckReportsDirectory
dependencyCheckReportsDirectory,
signedFilesDirectory
);

var buildFiles = new BuildFiles(
Expand Down Expand Up @@ -176,6 +179,7 @@ public class BuildDirectories
public DirectoryPath Packages { get; private set; }
public DirectoryPath EnvironmentSettings { get; private set; }
public DirectoryPath DependencyCheckReports { get; private set; }
public DirectoryPath SignedFiles { get; private set; }
public ICollection<DirectoryPath> ToClean { get; private set; }

public BuildDirectories(
Expand All @@ -199,7 +203,8 @@ public class BuildDirectories
DirectoryPath chocolateyPackages,
DirectoryPath packages,
DirectoryPath environmentSettings,
DirectoryPath dependencyCheckReports
DirectoryPath dependencyCheckReports,
DirectoryPath signedFiles
)
{
Build = build;
Expand All @@ -222,6 +227,7 @@ public class BuildDirectories
ChocolateyPackages = chocolateyPackages;
EnvironmentSettings = environmentSettings;
DependencyCheckReports = dependencyCheckReports;
SignedFiles = signedFiles;
Packages = packages;

ToClean = new[] {
Expand Down
37 changes: 34 additions & 3 deletions Chocolatey.Cake.Recipe/Content/sign-powershell.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,10 @@ Param(
[String[]]
$ScriptsToSign,

[Parameter()]
[String]
$OutputFolder,

[Parameter()]
[String]
$TimeStampServer,
Expand All @@ -40,11 +44,38 @@ Param(
$CertificateSubjectName
)

$cert = if ($PSCmdlet.ParameterSetName -eq "File") {
$Cert = if ($PSCmdlet.ParameterSetName -eq "File") {
New-Object System.Security.Cryptography.X509Certificates.X509Certificate2($CertificatePath, $CertificatePassword)
}
else {
Get-ChildItem Cert:\LocalMachine\My | Where-Object Subject -Like "*$CertificateSubjectName*"
Get-ChildItem Cert:\LocalMachine\My | Where-Object {$_.Subject -Like "*$CertificateSubjectName*" -and
$_.Issuer -match 'DigiCert' -and
$_.NotAfter -ge [datetime]::Now}
}

Set-AuthenticodeSignature -FilePath $ScriptsToSign -Cert $cert -TimestampServer $TimeStampServer -IncludeChain NotRoot -HashAlgorithm $CertificateAlgorithm
if ($Cert) {
$CommonSignParams = @{
'TimestampServer' = $TimeStampServer
'IncludeChain' = 'NotRoot'
'HashAlgorithm' = $CertificateAlgorithm
'Cert' = $Cert
}

foreach ($Script in $ScriptsToSign) {
$ExistingSig = Get-AuthenticodeSignature -FilePath $Script

if ($ExistingSig.Status -ne 'Valid' -or $ExistingSig.SignerCertificate.Issuer -notmatch 'DigiCert' -or $ExistingSig.SignerCertificate.NotAfter -lt [datetime]::Now) {
$NewSig = Set-AuthenticodeSignature -FilePath $Script @CommonSignParams
Write-Host "Script file '$Script' signed with status: $($NewSig.Status)"

if (!(Test-Path -Path $OutputFolder)) {
$null = New-Item -Path $OutputFolder -Type Directory
}
Copy-Item -Path $Script -Destination $OutputFolder
} else {
Write-Host "Script file '$Script' does not need signing, current signature is valid."
}
}
} else {
Write-Warning "Skipping script signing, no currently valid DigiCert issued Authenticode signing certificate matching '$($CertificateSubjectName)' was found."
}
38 changes: 38 additions & 0 deletions Chocolatey.Cake.Recipe/Content/sign.cake
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,37 @@
// See the License for the specific language governing permissions and
// limitations under the License.

BuildParameters.Tasks.VerifyPowerShellScriptsTask = Task("Verify-PowerShellScripts")
.WithCriteria(() => BuildParameters.ShouldVerifyPowerShellScripts, "Skipping since verifying PowerShell scripts has been disabled")
.Does(() =>
{
if (BuildParameters.GetScriptsToVerify != null)
{
var powerShellVerifyScript = GetFiles("./tools/Chocolatey.Cake.Recipe*/Content/verify-powershell.ps1").FirstOrDefault();

if (powerShellVerifyScript == null)
{
Warning("Unable to find PowerShell verification script, so unable to verify PowerShell scripts.");
return;
}

var scriptsToVerify = new List<string>();
foreach (var filePath in BuildParameters.GetScriptsToVerify())
{
scriptsToVerify.Add(MakeAbsolute(filePath).FullPath);
}

StartPowershellFile(MakeAbsolute(powerShellVerifyScript), args =>
{
args.AppendArray("ScriptsToVerify", scriptsToVerify);
});
}
else
{
Information("There are no PowerShell Scripts defined to be verified.");
}
});

BuildParameters.Tasks.SignPowerShellScriptsTask = Task("Sign-PowerShellScripts")
.WithCriteria(() => (!string.IsNullOrWhiteSpace(BuildParameters.CertificateFilePath) && FileExists(BuildParameters.CertificateFilePath)) || BuildSystem.IsRunningOnTeamCity, "Skipping because unable to find certificate, and not running on TeamCity")
.WithCriteria(() => BuildParameters.ShouldAuthenticodeSignPowerShellScripts, "Skipping since authenticode signing of PowerShell scripts has been disabled")
Expand All @@ -39,6 +70,7 @@ BuildParameters.Tasks.SignPowerShellScriptsTask = Task("Sign-PowerShellScripts")
StartPowershellFile(MakeAbsolute(powerShellSigningScript), args =>
{
args.AppendArray("ScriptsToSign", scriptsToSign)
.Append("OutputFolder", BuildParameters.Paths.Directories.SignedFiles.FullPath)
.Append("TimeStampServer", BuildParameters.CertificateTimestampUrl)
.AppendQuoted("CertificateSubjectName", BuildParameters.CertificateSubjectName)
.Append("CertificateAlgorithm", BuildParameters.CertificateAlgorithm);
Expand All @@ -53,12 +85,18 @@ BuildParameters.Tasks.SignPowerShellScriptsTask = Task("Sign-PowerShellScripts")
StartPowershellFile(MakeAbsolute(powerShellSigningScript), args =>
{
args.AppendArray("ScriptsToSign", scriptsToSign)
.Append("OutputFolder", BuildParameters.Paths.Directories.SignedFiles.FullPath)
.Append("TimeStampServer", BuildParameters.CertificateTimestampUrl)
.Append("CertificatePath", BuildParameters.CertificateFilePath)
.AppendSecret("CertificatePassword", password)
.Append("CertificateAlgorithm", BuildParameters.CertificateAlgorithm);
});
}

foreach (var signedFile in GetFiles(BuildParameters.Paths.Directories.SignedFiles + "/**/*"))
{
BuildParameters.BuildProvider.UploadArtifact(signedFile);
}
}
else
{
Expand Down
1 change: 1 addition & 0 deletions Chocolatey.Cake.Recipe/Content/tasks.cake
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ public class BuildTasks
public CakeTaskBuilder StrongNameSignerTask { get; set; }

// Signing Tasks
public CakeTaskBuilder VerifyPowerShellScriptsTask { get; set; }
public CakeTaskBuilder SignPowerShellScriptsTask { get; set; }
public CakeTaskBuilder SignAssembliesTask { get; set; }
public CakeTaskBuilder SignMsisTask { get; set; }
Expand Down
46 changes: 46 additions & 0 deletions Chocolatey.Cake.Recipe/Content/verify-powershell.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
# Copyright © 2024 Chocolatey Software, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
#
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

[cmdletBinding()]
Param(
[Parameter()]
[String[]]
$ScriptsToVerify
)

$AllScriptsVerified = $true

Write-Output ""
Write-Output "========== Verifying PowerShell Scripts =========="
Write-Output ""

foreach ($ScriptToVerify in $ScriptsToVerify) {
$ExistingSig = Get-AuthenticodeSignature -FilePath $ScriptToVerify

if ($ExistingSig.Status -ne 'Valid' -or $ExistingSig.SignerCertificate.Issuer -notmatch 'DigiCert' -or $ExistingSig.SignerCertificate.NotAfter -lt [datetime]::Now) {
$AllScriptsVerified = $false
Write-Output "Script file '$ScriptToVerify' contains an invalid signature, which must be corrected before build can succeed."
} else {
Write-Output "Script file '$ScriptToVerify' does not need signing, current signature is valid."
}
}

Write-Output ""
Write-Output "========== Verification Complete =========="
Write-Output ""

if ($AllScriptsVerified -eq $false) {
throw "At least one PowerShell script had an invalid signature. Check output for details."
}

0 comments on commit 4b9f73b

Please sign in to comment.