PSAzureSignTool.psm1
#region public functions function Select-FilesToSign() { <# .SYNOPSIS Produces a file that containing a list of files to be code signed. .DESCRIPTION Produces a file that containing a list of files to be code signed. Searches the provided root directory, and selects files based on regex pattern parameters, which do not already have a valid authenticode signature. .PARAMETER Path System.String; Required - The root path to begin searching for files to code sign. .PARAMETER Includes System.String; Optional - Default value is '.*' (matching everything). Each file which has a full path matching this regular expression pattern will be signed, if: 1. The file extension matches an extension Extensions 2. The file full name does not match the optional 'Excludes' pattern .PARAMETER Excludes System.String; Optional - Files with a full name matching this optional pattern will be excluded from code signing. .PARAMETER Extensions System.String; Optional - Comma-separated list of file extensions to include in search for files to sign. .PARAMETER OutputFileList System.String; Required - Full path for text file which will contain list of files that require an authenticode signature. .EXAMPLE # Searches C:\src\bin\Release, using default parameter values; # C:\files-to-sign.txt will be created. Any unsigned file with an extension # of .exe, .dll, .ocx, or .cab. Select-FilesToSign -Path C:\src\bin\Release -OutputFileList C:\files-to-sign.txt .EXAMPLE # Searches C:\src\bin\Release, using default parameter values; # C:\files-to-sign.txt will be created. Any unsigned file with an extension # of .exe, .dll, .ocx, or .cab that contains 'Fabrikam' or 'FabrikamFiber' # in the full file name Select-FilesToSign -Path C:\src\bin\Release -Includes "Fabrikam(Fiber)?" -OutputFileList C:\files-to-sign.txt .EXAMPLE # Searches C:\src\bin\Release, using default parameter values; # C:\files-to-sign.txt will be created. Any unsigned file with an extension # of .exe, .dll, .ocx, or .cab that contains 'Fabrikam' or 'FabrikamFiber' # that does not include 'Test' in the full file name Select-FilesToSign -Path C:\src\bin\Release -Includes "Fabrikam(Fiber)?" -Excludes "Test" -OutputFileList C:\files-to-sign.txt #> [CmdletBinding()] Param( [Alias("pa")] [Parameter(Mandatory=$true)] [ValidateNotNullOrEmpty()] [ValidateScript({Test-Path $_})] [string]$Path, [Alias("inf")] [ValidateNotNullOrEmpty()] [string]$Includes = ".*", [Alias("exf")] [ValidateNotNullOrEmpty()] [string]$Excludes = "^$", [Alias("ext")] [ValidateNotNullOrEmpty()] [string]$Extensions = ".exe,.dll,.ocx,.cab", [Alias("ofl")] [Parameter(Mandatory=$true)] [ValidateNotNullOrEmpty()] [string]$OutputFileList ) [string[]]$ArrExtensions = $Extensions -split "," New-Item -Path (Split-Path -Path $OutputFileList -Parent) -ItemType Directory -Force | Out-Null [psobject[]]$ToSign = Get-ChildItem -Path $Path -Recurse ` | Where-Object {($_.FullName -match $Includes) ` -and ($_.FullName -notmatch $Excludes) ` -and ($_.Extension -in $ArrExtensions)} if($ToSign.Count -gt 0) { $FilesToSign = $ToSign | Test-AuthenticodeSignature | Where-Object {($_.Valid -eq $false)} if($FilesToSign.Count -gt 0) { $FilesToSign.Path | Out-File -FilePath $OutputFileList -Encoding utf8 -Force Write-Host "`nList of files to sign saved to $OutputFileList `n" } else { throw "All files are already signed." } } else { throw "No files found to sign." } } function Start-CodeSign() { <# .SYNOPSIS Invokes AzureSignTool to sign and timestamp a list of files. For detailed parameter descriptions, please refer to the AzureSignTool documentaiton: https://github.com/vcsjones/AzureSignTool .PARAMETER AzureKeyVaultUrl System.String; Required - Corresponds to the AzureKeyVault '--azure-key-vault-url' parameter. For details see https://github.com/vcsjones/AzureSignTool#parameters .PARAMETER AzureKeyVaultClientId System.String; Required - Corresponds to the AzureKeyVault '--azure-key-vault-client-id' parameter. For details see https://github.com/vcsjones/AzureSignTool#parameters .PARAMETER AzureKeyVaultTenantId System.String; Required - Corresponds to the AzureKeyVault '--azure-key-vault-tenant-id' parameter. For details see https://github.com/vcsjones/AzureSignTool#parameters .PARAMETER AzureKeyVaultClientSecret System.String; Required - Corresponds to the AzureKeyVault '--azure-key-vault-client-secret' parameter. For details see https://github.com/vcsjones/AzureSignTool#parameters .PARAMETER AzureKeyVaultCertificate System.String; Required - Corresponds to the AzureKeyVault '--azure-key-vault-certificate' parameter. For details see https://github.com/vcsjones/AzureSignTool#parameters .PARAMETER InputFileList System.String; Required - Corresponds to the AzureKeyVault '--input-file-list' parameter. For details see https://github.com/vcsjones/AzureSignTool#parameters .PARAMETER TimestampUrl System.String; Optional - Corresponds to the AzureKeyVault '--azure-key-vault-client-id' parameter. For details see https://github.com/vcsjones/AzureSignTool#parameters .PARAMETER Description System.String; Optional - Corresponds to the AzureKeyVault '--description' parameter. For details see https://github.com/vcsjones/AzureSignTool#parameters .PARAMETER MaxParallelism System.String; Optional - Corresponds to the AzureKeyVault '--max-degree-of-parallelism' parameter. For details see https://github.com/vcsjones/AzureSignTool#parameters .PARAMETER MaxRetries System.Int16; Optional - Maximum number of retries. Minimum value is 0, maxiumum is 25. Defaults to 3. .PARAMETER BackoffRateSeconds System.Int16; Optional - Backoff rate, in seconds. Multiplied by the attempt index. Minimum value is 10, maximum 600. Defaults to 10. #> [CmdletBinding()] Param( [Alias("kvu")] [Parameter(Mandatory=$true)] [ValidateNotNullOrEmpty()] [string]$AzureKeyVaultUrl, [Alias("kvi")] [Parameter(Mandatory=$true)] [ValidateNotNullOrEmpty()] [string]$AzureKeyVaultClientId, [Alias("kvt")] [Parameter(Mandatory=$true)] [ValidateNotNullOrEmpty()] [string]$AzureKeyVaultTenantId, [Alias("kvs")] [Parameter(Mandatory=$true)] [ValidateNotNullOrEmpty()] [string]$AzureKeyVaultClientSecret, [Alias("kvc")] [Parameter(Mandatory=$true)] [ValidateNotNullOrEmpty()] [string]$AzureKeyVaultCertificate, [Alias("ifl")] [Parameter(Mandatory=$true)] [ValidateNotNullOrEmpty()] [string]$InputFileList, [Alias("tr")] [string]$TimestampUrl, [Alias("d")] [string]$Description, [Alias("mdop")] [string]$MaxParallelism, [Alias("maxr")] [ValidateNotNullOrEmpty()] [Parameter(Mandatory=$false)] [ValidateRange(0,25)] [int16]$MaxRetries = 3, [Alias("brs")] [ValidateNotNullOrEmpty()] [Parameter(Mandatory=$false)] [ValidateRange(10,600)] [int16]$BackoffRateSeconds = 10 ) [string[]]$SignArguments = @( 'sign', '-coe', '-v', '-fd',"sha256", '-kvu',$AzureKeyVaultUrl, '-kvi',$AzureKeyVaultClientId, '-kvt',$AzureKeyVaultTenantId, '-kvs',$AzureKeyVaultClientSecret, '-kvc',$AzureKeyVaultCertificate ) if($TimestampUrl) { $SignArguments += @('-tr',$TimestampUrl) } else { Write-host "Warning: TimestampUrl is not provided, only digital signature will be applied to assemblies." } if($MaxParallelism) { $SignArguments += @('-mdop',$MaxParallelism) } if($Description) { $SignArguments += @('-d',$Description) } Start-AzureSignTool -InputFileList $InputFileList ` -SignArguments $SignArguments Verify-Signatures -InputFileList $InputFileList ` -SignArguments $SignArguments ` -MaxRetries $MaxRetries ` -BackoffRateSeconds $BackoffRateSeconds } #endregion public functions #region private functions function Install-AzureSignTool() { # default AzureSignTool version, if AZURE_SIGN_TOOL_VERSION doesn't override [string]$ToolVersion = "3.0.0" [string]$ToolVersionFromEnv = [System.Environment]::GetEnvironmentVariable('AZURE_SIGN_TOOL_VERSION','Machine') # if AZURE_SIGN_TOOL_VERSION environment variable is set, use that value if($ToolVersionFromEnv) { Write-Host "Environment variable AZURE_SIGN_TOOL_VERSION set to $ToolVersionFromEnv; Using that value for AzureSignTool version." $ToolVersion = $ToolVersionFromEnv } [string]$ToolPath = "{0}\tools\azuresigntool\{1}" -f $env:USERPROFILE,$ToolVersion Write-Host ("Checking {0} for existing AzureSignTool installation..." -f $ToolPath) if(-not (Test-Path -Path $ToolPath)) { New-Item -Path $ToolPath -ItemType Directory -Force | Out-Null } if((Get-ChildItem -Path $ToolPath -File -Recurse).Count -eq "0") { Write-Host ("AzureSignTool version {0} is not available, installing..." -f $ToolVersion) [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 $output = dotnet tool install --tool-path $ToolPath AzureSignTool --version $ToolVersion Write-Host $output } else { Write-Host ("AzureSignTool version {0} is installed." -f $ToolVersion) Write-Debug ((Get-ChildItem -Path $ToolPath).FullName -join ", ") } Return $ToolPath } function Test-AuthenticodeSignature() { <# .SYNOPSIS Validates the authenticode signature and timestamps of a list of file paths .PARAMETER FullName System.String; Required - full path to a file being verified #> [CmdletBinding()] Param( [parameter(ValueFromPipelineByPropertyName,ValueFromPipeline)] [ValidateScript({Test-Path $_})] [string]$FullName ) Process { [psobject[]]$SigErrors = $() $Signature = $FullName | Get-AuthenticodeSignature $File = @{} $File.Path = $FullName if ($Signature.Status.ToString() -ne 'Valid') { $File.Issue = "Signature invalid" $File.Valid = $false $SigErrors += $File } elseIf (!$Signature.TimeStamperCertificate) { $File.Issue = "Timestamp certificate missing" $File.Valid = $false $SigErrors += $File } else { $File.Valid = $true Write-Host ("Signature/timestamp valid: {0}" -f $FullName) } Return $SigErrors } } function Start-AzureSignTool() { <# .SYNOPSIS Call AzureSignTool module to code sign assemblies .PARAMETER SignArguments System.String array; Required - array of parameters required to start code signing #> [CmdletBinding()] Param( [Parameter(Mandatory=$true)] [ValidateNotNullOrEmpty()] [string]$InputFileList, [ValidateNotNullOrEmpty()] [Parameter(Mandatory=$true)] [string[]]$SignArguments ) $SignArguments += @('-ifl',$InputFileList) [string]$ToolPath = Install-AzureSignTool $pscore = $false try{ if ((pwsh -version)){ $pscore = $true } } catch{} if($pscore){ pwsh -Command "& $ToolPath\AzureSignTool $SignArguments" } else{ powershell -Command "& $ToolPath\AzureSignTool $SignArguments" } } function Verify-Signatures() { <# .SYNOPSIS Validate signatures and retry code signing if needed .PARAMETER InputFileList System.String; Required - Text file containing newline-separated list of full paths of files. Each file must have a valid authenticode signature and timestamp or the function will throw an exception. .PARAMETER SignArguments System.String array; Required - array of parameters required to start code signing .PARAMETER RetryIdx System.Int16; Optional - amount of retries which already took place #> [CmdletBinding()] Param( [ValidateNotNullOrEmpty()] [Parameter(Mandatory=$true)] [string]$InputFileList, [ValidateNotNullOrEmpty()] [Parameter(Mandatory=$true)] [string[]]$SignArguments, [ValidateNotNullOrEmpty()] [Parameter(Mandatory=$false)] [ValidateRange(0,25)] [int16]$MaxRetries = 3, [ValidateNotNullOrEmpty()] [Parameter(Mandatory=$false)] [ValidateRange(10,600)] [int16]$BackoffRateSeconds = 10, [ValidateNotNullOrEmpty()] [Parameter(Mandatory=$false)] [int16]$RetryIdx = 0 ) [psobject[]]$ToSign = Get-ChildItem -Path (Get-Content $InputFileList) ` | Test-AuthenticodeSignature ` | Where-Object {($_.Valid -eq $false)} if($ToSign.Count -gt 0) { if($RetryIdx -lt $MaxRetries) { $RetryIdx ++ Write-Host "`nNot all files are code signed ($($ToSign.Count)), retrying... (attempt $($RetryIdx) of $($MaxRetries))" $RetryOutputFileList = (Split-Path -Path $InputFileList) + "\FilesToSign_Retry_$($RetryIdx).txt" $ToSign.Path | Out-File -FilePath $RetryOutputFileList -Encoding utf8 -Force $SecondsWait = $BackoffRateSeconds * $RetryIdx Write-Host "Retry $($RetryIdx), in $($SecondsWait) seconds" Start-Sleep -s $SecondsWait Start-AzureSignTool -InputFileList $RetryOutputFileList ` -SignArguments $SignArguments Verify-Signatures -InputFileList $RetryOutputFileList ` -SignArguments $SignArguments ` -MaxRetries $MaxRetries ` -BackoffRateSeconds $BackoffRateSeconds ` -RetryIdx $RetryIdx } else { throw "`nAmount of retries ($($RetryIdx)) has been exhausted, Code Signing failed.`n" } } } #endregion private functions Export-ModuleMember -Function Select-FilesToSign, ` Start-CodeSign |