#Requires -Version 5.1 [CmdletBinding()] param( [Parameter(Mandatory = $true)] [ValidatePattern('^(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)$')] [string]$ExpectedVersion, [Parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] [string]$ExpectedSignerSubject, [Parameter(Mandatory = $false)] [string]$ReleaseDirectory ) Set-StrictMode -Version Latest $ErrorActionPreference = 'Stop' function Fail-Verification { param([Parameter(Mandatory = $true)][string]$Reason) throw "windows_release_verification_failed:$Reason" } function Get-RequiredYamlValue { param( [Parameter(Mandatory = $true)][string]$Text, [Parameter(Mandatory = $true)][string]$Pattern, [Parameter(Mandatory = $true)][string]$Name ) $matches = [regex]::Matches($Text, $Pattern) if ($matches.Count -ne 1) { Fail-Verification "latest_yml_${Name}_count_$($matches.Count)" } return $matches[0].Groups[1].Value.Trim().Trim('"').Trim("'") } function Get-Sha512Base64 { param([Parameter(Mandatory = $true)][string]$Path) $stream = [System.IO.File]::OpenRead($Path) $sha512 = [System.Security.Cryptography.SHA512]::Create() try { return [Convert]::ToBase64String($sha512.ComputeHash($stream)) } finally { $sha512.Dispose() $stream.Dispose() } } function Assert-PeVersion { param( [Parameter(Mandatory = $true)][string]$Path, [Parameter(Mandatory = $true)][string]$Label, [Parameter(Mandatory = $true)][version]$Expected ) $versionInfo = (Get-Item -LiteralPath $Path).VersionInfo foreach ($entry in @( @{ Name = 'file_version'; Value = $versionInfo.FileVersion }, @{ Name = 'product_version'; Value = $versionInfo.ProductVersion } )) { $value = [string]$entry.Value if ($value -notmatch '^\d+\.\d+\.\d+(?:\.0)?$') { Fail-Verification "${Label}_$($entry.Name)_invalid" } $actual = [version]$value if ($actual.Major -ne $Expected.Major -or $actual.Minor -ne $Expected.Minor -or $actual.Build -ne $Expected.Build -or ($actual.Revision -notin @(-1, 0))) { Fail-Verification "${Label}_$($entry.Name)_mismatch" } } if ($versionInfo.ProductName -cne 'D3RO Voice') { Fail-Verification "${Label}_product_name_mismatch" } } function Assert-ProductionSignature { param( [Parameter(Mandatory = $true)][string]$Path, [Parameter(Mandatory = $true)][string]$Label, [Parameter(Mandatory = $true)][string]$ExpectedSubject ) $signature = Get-AuthenticodeSignature -LiteralPath $Path if ($signature.Status -ne [System.Management.Automation.SignatureStatus]::Valid) { Fail-Verification "${Label}_authenticode_$($signature.Status)" } $certificate = $signature.SignerCertificate if ($null -eq $certificate) { Fail-Verification "${Label}_signer_certificate_missing" } $actualSubject = $certificate.Subject.Trim() if (-not [string]::Equals($actualSubject, $ExpectedSubject, [StringComparison]::OrdinalIgnoreCase)) { Fail-Verification "${Label}_signer_subject_mismatch" } if ([string]::Equals($certificate.Subject, $certificate.Issuer, [StringComparison]::OrdinalIgnoreCase)) { Fail-Verification "${Label}_self_signed_certificate" } $now = [DateTime]::UtcNow if ($certificate.NotBefore.ToUniversalTime() -gt $now -or $certificate.NotAfter.ToUniversalTime() -le $now) { Fail-Verification "${Label}_signer_certificate_not_current" } $codeSigningOid = '1.3.6.1.5.5.7.3.3' $ekuOids = @() foreach ($extension in $certificate.Extensions) { if ($extension -is [System.Security.Cryptography.X509Certificates.X509EnhancedKeyUsageExtension]) { foreach ($usage in $extension.EnhancedKeyUsages) { $ekuOids += $usage.Value } } } if ($ekuOids -notcontains $codeSigningOid) { Fail-Verification "${Label}_code_signing_eku_missing" } return $certificate } $expectedSubject = $ExpectedSignerSubject.Trim() if ([string]::IsNullOrWhiteSpace($expectedSubject)) { Fail-Verification 'expected_signer_subject_empty' } if ($expectedSubject -match '(?i)Everything2EverythingDev') { Fail-Verification 'development_signer_subject_forbidden' } if ([string]::IsNullOrWhiteSpace($ReleaseDirectory)) { $ReleaseDirectory = Join-Path $PSScriptRoot "../../apps/desktop/release/$ExpectedVersion" } if (-not (Test-Path -LiteralPath $ReleaseDirectory -PathType Container)) { Fail-Verification 'release_directory_missing' } $releasePath = (Resolve-Path -LiteralPath $ReleaseDirectory).Path if ((Split-Path -Leaf $releasePath) -cne $ExpectedVersion) { Fail-Verification 'release_directory_version_mismatch' } $installerName = "D3RO-Voice-Setup-$ExpectedVersion-x64.exe" $installerPath = Join-Path $releasePath $installerName $latestPath = Join-Path $releasePath 'latest.yml' $appPath = Join-Path $releasePath 'win-unpacked/D3RO Voice.exe' foreach ($requiredFile in @($installerPath, $latestPath, $appPath)) { if (-not (Test-Path -LiteralPath $requiredFile -PathType Leaf)) { Fail-Verification "required_file_missing_$(Split-Path -Leaf $requiredFile)" } } $installers = @(Get-ChildItem -LiteralPath $releasePath -File -Filter 'D3RO-Voice-Setup-*-x64.exe') if ($installers.Count -ne 1 -or $installers[0].Name -cne $installerName) { Fail-Verification 'installer_set_not_exact' } $latestText = [System.IO.File]::ReadAllText($latestPath) $metadataVersion = Get-RequiredYamlValue $latestText '(?m)^version:\s*([^\r\n#]+?)\s*$' 'version' $metadataUrl = Get-RequiredYamlValue $latestText '(?m)^\s{2}-\s+url:\s*([^\r\n#]+?)\s*$' 'file_url' $metadataFileSha512 = Get-RequiredYamlValue $latestText '(?m)^\s{4}sha512:\s*([^\r\n#]+?)\s*$' 'file_sha512' $metadataSizeText = Get-RequiredYamlValue $latestText '(?m)^\s{4}size:\s*([^\r\n#]+?)\s*$' 'file_size' $metadataPath = Get-RequiredYamlValue $latestText '(?m)^path:\s*([^\r\n#]+?)\s*$' 'path' $metadataSha512 = Get-RequiredYamlValue $latestText '(?m)^sha512:\s*([^\r\n#]+?)\s*$' 'sha512' if ($metadataVersion -cne $ExpectedVersion) { Fail-Verification 'latest_yml_version_mismatch' } if ($metadataUrl -cne $installerName -or $metadataPath -cne $installerName) { Fail-Verification 'latest_yml_installer_reference_mismatch' } [long]$metadataSize = 0 if (-not [long]::TryParse($metadataSizeText, [ref]$metadataSize) -or $metadataSize -le 0) { Fail-Verification 'latest_yml_size_invalid' } $installerSize = (Get-Item -LiteralPath $installerPath).Length if ($metadataSize -ne $installerSize) { Fail-Verification 'latest_yml_size_mismatch' } $installerSha512 = Get-Sha512Base64 $installerPath if ($metadataFileSha512 -cne $installerSha512 -or $metadataSha512 -cne $installerSha512) { Fail-Verification 'latest_yml_sha512_mismatch' } $expectedPeVersion = [version]"$ExpectedVersion.0" Assert-PeVersion $installerPath 'installer' $expectedPeVersion Assert-PeVersion $appPath 'unpacked_app' $expectedPeVersion $installerCertificate = Assert-ProductionSignature $installerPath 'installer' $expectedSubject $appCertificate = Assert-ProductionSignature $appPath 'unpacked_app' $expectedSubject if ($installerCertificate.Thumbprint -cne $appCertificate.Thumbprint) { Fail-Verification 'signer_thumbprint_mismatch' } $evidence = [ordered]@{ schemaVersion = 1 version = $ExpectedVersion installer = $installerName installerSize = $installerSize installerSha512 = $installerSha512 unpackedApp = 'win-unpacked/D3RO Voice.exe' authenticodeStatus = 'Valid' signerSubject = $installerCertificate.Subject signerIssuer = $installerCertificate.Issuer signerThumbprint = $installerCertificate.Thumbprint verifiedAtUtc = [DateTime]::UtcNow.ToString('o') } $evidence | ConvertTo-Json -Depth 3