PSAzureSignTool.psm1


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[]]$ToSign = @()
    [string[]]$ArrExtensions = $Extensions -split ","

    New-Item -Path (Split-Path -Path $OutputFileList -Parent) -ItemType Directory -Force | Out-Null

    Get-ChildItem -Path $Path -File -Recurse `
      | Where-Object {($_.FullName -match $Includes) `
          -and ($_.FullName -notmatch $Excludes) `
          -and ($_.Extension -in $ArrExtensions)} `
      | ForEach-Object {
        $signStatus = (Get-AuthenticodeSignature -FilePath $_.FullName).Status   
        if($signStatus -ne "Valid")
        {
          [string]$ToAdd = Get-Item $_.FullName
          Write-Host "$signStatus - $ToAdd"
          $ToSign += $ToAdd
        }
      }

    if($ToSign)
    {
      Write-Host "`nList of files to sign saved to $OutputFileList `n"
      $ToSign | Out-File -FilePath $OutputFileList -Encoding utf8 -Force
    }
    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 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
    #>


    [CmdletBinding()]
    Param(       
        [Alias("kvu")]
        [Parameter(Mandatory=$true)]
        [ValidateNotNullOrEmpty()]
        [string]$AzureKeyVaultUrl,

        [Alias("kvi")]
        [Parameter(Mandatory=$true)]
        [ValidateNotNullOrEmpty()]
        [string]$AzureKeyVaultClientId,

        [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        
    )
        
    try
    {
        AzureSignTool
        Write-Host "AzureSignTool is installed."
    }
    catch
    {
        Write-Host "AzureSignTool is not available, installing..."
        dotnet tool install --global AzureSignTool    
    }    

    [string[]]$SignArguments = @(
        'sign',
        '-v',
        '-fd',"sha256",
        '-kvu',$AzureKeyVaultUrl,
        '-kvi',$AzureKeyVaultClientId,  
        '-kvs',$AzureKeyVaultClientSecret,
        '-kvc',$AzureKeyVaultCertificate,
        '-ifl',$InputFileList
    )

    if($TimestampUrl)
    {
        $SignArguments += @('-tr',$TimestampUrl)
    }
    else
    {
        Write-host "Warning: TimestampUrl is not provided, only digital signature will be applied to assemblies."
    }

    $pscore = $false
    try{
        if ((pwsh -version)){
            $pscore = $true
        }
    }
    catch{}
    
    if($pscore){
        pwsh -Command "& AzureSignTool $SignArguments"  
    }
    else{
        powershell -Command "& AzureSignTool $SignArguments"
    }
}

function Test-AuthenticodeSignature()
{
    <#
    .SYNOPSIS
    Validates the authenticode signature and timestamps of a list of file paths

    .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.

    .EXAMPLE
    # Validates that each file listed in C:\files-to-sign.txt has a valid
    # authenticode signature and timestamp
    Test-AuthenticodeSignature -InputFileList C:\files-to-sign.txt
    #>


    [CmdletBinding()]
    Param( 
        [Alias("ifl")]
        [ValidateNotNullOrEmpty()]
        [string]$InputFileList
    )

    [string[]]$SigErrors = @()

    Write-Host "`nStarting validation..."

    $ToSignArray = ((Get-Content $InputFileList) -join ',') -split ','
    Get-AuthenticodeSignature -FilePath $ToSignArray | ForEach-Object {
        if ($_.Status.ToString() -ne 'Valid')
        {
            $ErrMsg = "Signature invalid: $($_.Path)"
            $SigErrors += $ErrMsg

            Write-Host $ErrMsg            
        }
        elseIf ($TimestampUrl -and (!$_.TimeStamperCertificate))
        {
            $ErrMsg = "Timestamp certificate missing: $($_.Path)"            
            $SigErrors += $ErrMsg

            Write-Host $ErrMsg
        }
        else
        {
            Write-Host ("Signature/timestamp valid: {0}" -f $_.Path)
        }
    }

    if($SigErrors -gt 0)
    {
        throw "`nValidation failed."
    }
    else
    {
        Write-Host "`nValidation completed successfully!"             
    }
}

Export-ModuleMember -Function Select-FilesToSign, `
                              Start-CodeSign, `
                              Test-AuthenticodeSignature