Private/Get-OSDAppBuiltInSignature.ps1
|
function Get-OSDAppExpectedPublisher { [CmdletBinding()] param([Parameter(Mandatory)][string]$Id) # Expected certificate organization. No thumbprint pinning: vendor # signing certificates rotate. Treat a changed publisher as a release # blocker requiring review, not a reason to silently trust a new one. switch ($Id) { 'Microsoft365Apps' { 'Microsoft Corporation'; break } 'MicrosoftTeams' { 'Microsoft Corporation'; break } 'GoogleChromeEnterprise' { 'Google LLC'; break } 'CiscoWebex' { 'Cisco Systems, Inc.'; break } 'MozillaFirefoxEnterprise' { 'Mozilla Corporation'; break } default { $null } } } function Test-OSDAppExpectedPublisher { [CmdletBinding()] param( [Parameter(Mandatory)][string]$Id, [string]$SignerSubject ) $expected = Get-OSDAppExpectedPublisher -Id $Id if (-not $expected -or [string]::IsNullOrWhiteSpace($SignerSubject)) { return $false } # Extract the O= organization attribute, accepting quoted commas in an # RFC 2253-style subject such as Cisco Systems, Inc. Other attributes # including OU and CN do NOT grant publisher approval. $match = [regex]::Match($SignerSubject, '(?:^|,\s*)O=(?:"(?<quoted>(?:[^"]|"")*)"|(?<plain>[^,]+))(?=,|$)') if (-not $match.Success) { return $false } $org = if ($match.Groups['quoted'].Success) { $match.Groups['quoted'].Value.Replace('""','"') } else { $match.Groups['plain'].Value.Trim() } return [string]::Equals($org,$expected,[StringComparison]::OrdinalIgnoreCase) } function Assert-OSDAppBuiltInSignature { [CmdletBinding()] param( [Parameter(Mandatory)][string]$Id, [Parameter(Mandatory)][string]$FilePath ) $signature = Get-OSDAppBuiltInSignature -Id $Id -FilePath $FilePath -CheckPublisher if ($signature.Status -ne 'Valid') { throw "Built-in '$Id' was rejected: signature status '$($signature.Status)' for '$FilePath'. Expected publisher: $(Get-OSDAppExpectedPublisher -Id $Id). Actual signer: $($signature.Signer)." } return $signature } function Get-OSDAppBuiltInSignature { <# .SYNOPSIS Inspects a cached built-in package's Authenticode signature without changing it. .NOTES Windows trust evaluation is environment- and time-dependent. A valid signature is not a guarantee of a current package or malware-free content. Windows may contact a revocation service when checking certificate trust. #> [CmdletBinding()] param( [Parameter(Mandatory)][string]$Id, [string]$CacheEntryPath, [string]$FilePath, [switch]$CheckPublisher ) $relativeFile = switch ($Id) { 'Microsoft365Apps' { 'setup.exe'; break } 'MicrosoftTeams' { 'teamsbootstrapper.exe'; break } 'GoogleChromeEnterprise' { 'Package.msi'; break } 'CiscoWebex' { 'Package.msi'; break } 'MozillaFirefoxEnterprise' { 'Package.msi'; break } default { $null } } if (-not $relativeFile) { return [pscustomobject]@{ Status='Not assessed'; Scope='ZIP / unsupported type' File=$null; Signer=$null; Detail='An Authenticode signature was not checked for this cache format.' } } $scope = if ($Id -eq 'MicrosoftTeams') { 'Bootstrapper EXE only (MSIX not checked)' } elseif ($Id -eq 'Microsoft365Apps') { 'Office setup.exe only (Office data not checked)' } elseif ($relativeFile.EndsWith('.msi')) { 'MSI package' } else { 'EXE file' } if (-not $FilePath -and -not $CacheEntryPath) { throw 'Specify -CacheEntryPath or -FilePath.' } $path = if ($FilePath) { $FilePath } else { Join-Path $CacheEntryPath $relativeFile } if (-not (Test-Path -LiteralPath $path -PathType Leaf)) { return [pscustomobject]@{ Status='File missing';Scope=$scope;File=$path;Signer=$null Detail='Required signature target is missing.' } } if (Test-OSDAppWinPE) { return [pscustomobject]@{ Status='Unavailable in WinPE';Scope=$scope;File=$path;Signer=$null Detail='Full Windows Authenticode inspection was not attempted in WinPE.' } } try { # Use the installed Windows trust provider in read-only mode. $signature = Get-AuthenticodeSignature -LiteralPath $path -ErrorAction Stop $status = [string]$signature.Status if (-not $status) { $status = 'UnknownError' } $signer = if ($signature.SignerCertificate) { [string]$signature.SignerCertificate.Subject } else { $null } $expectedPublisher = Get-OSDAppExpectedPublisher -Id $Id $publisherMatches = if ($signer -and $expectedPublisher) { Test-OSDAppExpectedPublisher -Id $Id -SignerSubject $signer } else { $false } if ($CheckPublisher -and $status -eq 'Valid' -and $expectedPublisher -and -not $publisherMatches) { $status = 'PublisherMismatch' } return [pscustomobject]@{ Status=$status Scope=$scope File=$path Signer=$signer ExpectedPublisher=$expectedPublisher PublisherMatches=$publisherMatches Detail=if ($status -eq 'PublisherMismatch') { "The valid signature was not issued to expected publisher '$expectedPublisher'." } elseif ($signature.StatusMessage) { [string]$signature.StatusMessage } else { $status } } } catch { Write-Verbose "Authenticode inspection unavailable for '$path': $($_.Exception.Message)" return [pscustomobject]@{ Status='Check failed';Scope=$scope;File=$path;Signer=$null Detail=$_.Exception.Message } } } |