TrustedSigning.psm1

using namespace System.Collections.Generic
using namespace System.IO

function Invoke-TrustedSigning {
    param (
        [Parameter(Mandatory)]
        [string]$Endpoint,

        [Parameter(Mandatory)]
        [string]$CodeSigningAccountName,

        [Parameter(Mandatory)]
        [string]$CertificateProfileName,

        [Parameter()]
        [AllowEmptyString()]
        [string]$Files,

        [Parameter()]
        [AllowEmptyString()]
        [string]$FilesFolder,

        [Parameter()]
        [AllowEmptyString()]
        [string]$FilesFolderFilter,

        [Parameter()]
        [switch]$FilesFolderRecurse = $false,

        [Parameter()]
        [AllowNull()]
        [int]$FilesFolderDepth,

        [Parameter()]
        [AllowEmptyString()]
        [string]$FilesCatalog,

        [Parameter(Mandatory)]
        [string]$FileDigest,

        [Parameter()]
        [AllowEmptyString()]
        [string]$TimestampRfc3161,

        [Parameter()]
        [AllowEmptyString()]
        [string]$TimestampDigest,

        [Parameter()]
        [switch]$AppendSignature = $false,

        [Parameter()]
        [AllowEmptyString()]
        [string]$Description,

        [Parameter()]
        [AllowEmptyString()]
        [string]$DescriptionUrl,

        [Parameter()]
        [AllowEmptyString()]
        [string]$GenerateDigestPath,

        [Parameter()]
        [switch]$GenerateDigestXml = $false,

        [Parameter()]
        [AllowEmptyString()]
        [string]$IngestDigestPath,

        [Parameter()]
        [switch]$SignDigest = $false,

        [Parameter()]
        [switch]$GeneratePageHashes = $false,

        [Parameter()]
        [switch]$SuppressPageHashes = $false,

        [Parameter()]
        [switch]$GeneratePkcs7 = $false,

        [Parameter()]
        [AllowEmptyString()]
        [string]$Pkcs7Options,

        [Parameter()]
        [AllowEmptyString()]
        [string]$Pkcs7Oid,

        [Parameter()]
        [AllowEmptyString()]
        [string]$EnhancedKeyUsage,

        [Parameter()]
        [switch]$ExcludeEnvironmentCredential = $false,

        [Parameter()]
        [switch]$ExcludeWorkloadIdentityCredential = $false,

        [Parameter()]
        [switch]$ExcludeManagedIdentityCredential = $false,

        [Parameter()]
        [switch]$ExcludeSharedTokenCacheCredential = $false,

        [Parameter()]
        [switch]$ExcludeVisualStudioCredential = $false,

        [Parameter()]
        [switch]$ExcludeVisualStudioCodeCredential = $false,

        [Parameter()]
        [switch]$ExcludeAzureCliCredential = $false,

        [Parameter()]
        [switch]$ExcludeAzurePowerShellCredential = $false,

        [Parameter()]
        [switch]$ExcludeAzureDeveloperCliCredential = $false,

        [Parameter()]
        [switch]$ExcludeInteractiveBrowserCredential = $false,

        [Parameter()]
        [int]$Timeout = 300,

        [Parameter()]
        [ValidateRange(0, 30000)]
        [int]$BatchSize = 10000
    )

    # Install required packages unless they already exist.
    Write-Information -MessageData "Checking for required dependencies." -InformationAction Continue
    $dependencies = Get-EveryDependency -Verbose
    $dlibFolderPath = $dependencies.DlibFolderPath
    $signToolFolderPath = $dependencies.SignToolFolderPath

    # Create the Trusted Signing metadata.json file that is passed to signtool.exe.
    $metadataFilePath = Join-Path -Path $dlibFolderPath -ChildPath "metadata.json"
    Write-Information -MessageData "`nCreating metadata file: $metadataFilePath" -InformationAction Continue
    $convertToMetadataJsonParams = @{
        Endpoint = $Endpoint
        CodeSigningAccountName = $CodeSigningAccountName
        CertificateProfileName = $CertificateProfileName
        ExcludeEnvironmentCredential = $ExcludeEnvironmentCredential
        ExcludeWorkloadIdentityCredential = $ExcludeWorkloadIdentityCredential
        ExcludeManagedIdentityCredential = $ExcludeManagedIdentityCredential
        ExcludeSharedTokenCacheCredential = $ExcludeSharedTokenCacheCredential
        ExcludeVisualStudioCredential = $ExcludeVisualStudioCredential
        ExcludeVisualStudioCodeCredential = $ExcludeVisualStudioCodeCredential
        ExcludeAzureCliCredential = $ExcludeAzureCliCredential
        ExcludeAzurePowerShellCredential = $ExcludeAzurePowerShellCredential
        ExcludeAzureDeveloperCliCredential = $ExcludeAzureDeveloperCliCredential
        ExcludeInteractiveBrowserCredential = $ExcludeInteractiveBrowserCredential
    }
    $metadataJson = ConvertTo-MetadataJson @convertToMetadataJsonParams
    [File]::WriteAllLines($metadataFilePath, $metadataJson)

    # Get the list of files to be signed from the files list, files folder and the
    # catalog file.

    # Files list.
    Write-Information -MessageData "`nGetting the list of files to be signed." -InformationAction Continue
    if ([string]::IsNullOrWhiteSpace($Files)) {
        Write-Information -MessageData "`tNo files list was provided." -InformationAction Continue
        $listedFiles = [List[string]]::new()
    } else {
        Write-Information -MessageData "`tGetting files from list." -InformationAction Continue
        $listedFiles = Get-FileList -Files $Files
    }
    Write-Information -MessageData "`tListed files: $($listedFiles.Count)" -InformationAction Continue
    foreach ($file in $listedFiles) {
        Write-Information -MessageData "`t`t$file" -InformationAction Continue
    }

    # Files folder.
    if ([string]::IsNullOrWhiteSpace($FilesFolder)) {
        Write-Information -MessageData "`n`tNo files folder was provided." -InformationAction Continue
        $filteredFiles = [List[string]]::new()
    } else {
        Write-Information -MessageData "`n`tGetting files from folder: $FilesFolder" -InformationAction Continue
        Write-Information -MessageData "`tFilter: $FilesFolderFilter" -InformationAction Continue
        $getFilteredFileListParams = @{
            Path = $FilesFolder
            Filter = $FilesFolderFilter
            Recurse = $FilesFolderRecurse
            Depth = $FilesFolderDepth
        }
        $filteredFiles = Get-FilteredFileList @getFilteredFileListParams
    }
    Write-Information -MessageData "`tFiltered files: $($filteredFiles.Count)" -InformationAction Continue
    foreach ($file in $filteredFiles) {
        Write-Information -MessageData "`t`t$file" -InformationAction Continue
    }

    # Catalog file.
    if ([string]::IsNullOrWhiteSpace($FilesCatalog)) {
        Write-Information -MessageData "`n`tNo catalog file was provided." -InformationAction Continue
        $catalogFiles = [List[string]]::new()
    } else {
        Write-Information -MessageData "`n`tGetting files from catalog file: $FilesCatalog" -InformationAction Continue
        $catalogFiles = Get-CatalogFileList -Path $FilesCatalog
    }
    Write-Information -MessageData "`tCatalog files: $($catalogFiles.Count)" -InformationAction Continue
    foreach ($file in $catalogFiles) {
        Write-Information -MessageData "`t`t$file" -InformationAction Continue
    }

    # Combine the list of files to be signed.
    Write-Information -MessageData "`nFormatting the list of files to be signed." -InformationAction Continue
    $formatFileListParams = @{
        ListedFiles = $listedFiles
        FilteredFiles = $filteredFiles
        CatalogFiles = $catalogFiles
    }
    $formattedFileList = Format-FileList @formatFileListParams

    # Batch the list of files to be signed.
    Write-Information -MessageData "`nBatching the list of files to be signed." -InformationAction Continue
    $batchedFileLists = Split-FileList -FormattedFiles $formattedFileList -BatchSize $BatchSize
    if ($batchedFileLists.Count -eq 0) {
        throw "No files were found to sign"
    }

    # Sign the files in each batch.
    $batchCounter = 1
    foreach ($batchedFileList in $batchedFileLists) {
        Write-Information -MessageData "`nSigning file batch $($batchCounter) of $($batchedFileLists.Count)." -InformationAction Continue
        # Format the arguments that will be passed to signtool.exe.
        $dlibFilePath = Join-Path -Path $dlibFolderPath -ChildPath "Azure.CodeSigning.Dlib.dll"
        $formatSignToolArgumentListParams = @{
            FileList = $batchedFileList
            FileDigest = $FileDigest
            TimestampRfc3161 = $TimestampRfc3161
            TimestampDigest = $TimestampDigest
            DlibFilePath = $dlibFilePath
            MetadataFilePath = $metadataFilePath
            AppendSignature = $AppendSignature
            Description = $Description
            DescriptionUrl = $DescriptionUrl
            GenerateDigestPath = $GenerateDigestPath
            GenerateDigestXml = $GenerateDigestXml
            IngestDigestPath = $IngestDigestPath
            SignDigest = $SignDigest
            GeneratePageHashes = $GeneratePageHashes
            SuppressPageHashes = $SuppressPageHashes
            GeneratePkcs7 = $GeneratePkcs7
            Pkcs7Options = $Pkcs7Options
            Pkcs7Oid = $Pkcs7Oid
            EnhancedKeyUsage = $EnhancedKeyUsage
        }
        $signToolArguments = Format-SignToolArgumentList @formatSignToolArgumentListParams

        # Run signtool.exe.
        $invokeSignToolParams = @{
            SignToolFolderPath = $signToolFolderPath
            SignToolArguments = $signToolArguments
            Timeout = $Timeout
        }
        $result = Invoke-SignTool @invokeSignToolParams

        # Throw an error if signtool.exe failed.
        # Exit code 0 means that signtool.exe completed successfully.
        # Exit code 2 means that signtool.exe completed with warnings.
        if ($result -ne 0 -and $result -ne 2) {
            throw "SignTool failed with exit code $($result)"
        }

        $batchCounter++
    }

    Write-Information -MessageData "`nTrusted Signing completed successfully" -InformationAction Continue
    return "Trusted Signing completed successfully"
}