221 lines
7.5 KiB
PowerShell
221 lines
7.5 KiB
PowerShell
#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
|