#requires -Version 5.1 [CmdletBinding(DefaultParameterSetName = 'AutoCertificate')] param( [Parameter(Mandatory, Position = 0)] [ValidateNotNullOrEmpty()] [string[]] $BinaryPath, [Parameter(Mandatory, ParameterSetName = 'CertificateStore')] [ValidatePattern('^[0-9A-Fa-f ]{40,64}$')] [string] $CertificateThumbprint, [Parameter(ParameterSetName = 'AutoCertificate')] [switch] $AllowDevelopmentCertificate, [Parameter(ParameterSetName = 'AutoCertificate')] [ValidateNotNullOrEmpty()] [string] $DevelopmentCertificateSubject = 'CN=VehicleHelper Development (local trust only)', [Parameter(ParameterSetName = 'AutoCertificate')] [ValidateScript({ -not $_ -or -not (Split-Path -Parent $_) -or (Test-Path -LiteralPath (Split-Path -Parent $_) -PathType Container) })] [string] $ExportDevelopmentCertificatePath, [Parameter(Mandatory, ParameterSetName = 'ArtifactSigning')] [ValidateScript({ Test-Path -LiteralPath $_ -PathType Leaf })] [string] $MetadataPath, [Parameter(Mandatory, ParameterSetName = 'ArtifactSigning')] [ValidateScript({ Test-Path -LiteralPath $_ -PathType Leaf })] [string] $DlibPath, [ValidateScript({ -not $_ -or (Test-Path -LiteralPath $_ -PathType Leaf) })] [string] $SignToolPath, [ValidateNotNullOrEmpty()] [string] $TimestampUrl = 'http://timestamp.acs.microsoft.com', [ValidateNotNullOrEmpty()] [string] $Description = 'VehicleHelper' ) Set-StrictMode -Version Latest $ErrorActionPreference = 'Stop' function Resolve-SignTool { if ($SignToolPath) { return (Resolve-Path -LiteralPath $SignToolPath).Path } $command = Get-Command signtool.exe -ErrorAction SilentlyContinue if ($command) { return $command.Source } $programFilesX86 = [Environment]::GetFolderPath('ProgramFilesX86') $sdkBin = Join-Path $programFilesX86 'Windows Kits\10\bin' if (Test-Path -LiteralPath $sdkBin -PathType Container) { $candidate = Get-ChildItem -LiteralPath $sdkBin -Directory | Sort-Object Name -Descending | ForEach-Object { Join-Path $_.FullName 'x64\signtool.exe' } | Where-Object { Test-Path -LiteralPath $_ -PathType Leaf } | Select-Object -First 1 if ($candidate) { return $candidate } } throw 'signtool.exe was not found. Install the Windows SDK or pass -SignToolPath.' } function Get-CodeSigningCertificates { $now = Get-Date $certificates = @() foreach ($store in @('Cert:\CurrentUser\My', 'Cert:\LocalMachine\My')) { if (-not (Test-Path -LiteralPath $store)) { continue } $scope = if ($store -like '*LocalMachine*') { 'LocalMachine' } else { 'CurrentUser' } $certificates += Get-ChildItem -LiteralPath $store -ErrorAction SilentlyContinue | Where-Object { $_.HasPrivateKey -and $_.NotBefore -le $now -and $_.NotAfter -gt $now -and ($_.EnhancedKeyUsageList | Where-Object { $oid = $_.ObjectId $valueProperty = $oid.PSObject.Properties['Value'] $oidValue = if ($valueProperty) { [string]$valueProperty.Value } else { [string]$oid } $oidValue -eq '1.3.6.1.5.5.7.3.3' }) } | ForEach-Object { $isDevelopment = $_.Subject -eq $DevelopmentCertificateSubject -or $_.Subject -eq 'CN=VehicleHelper Development (local trust only)' [pscustomobject]@{ Certificate = $_ Scope = $scope Development = $isDevelopment PublicCertificatePath = '' } } } return @($certificates | Sort-Object @{ Expression = { $_.Scope -eq 'CurrentUser' }; Descending = $true }, @{ Expression = { $_.Certificate.NotAfter }; Descending = $true }) } function Resolve-CodeSigningCertificate { param( [string] $Thumbprint, [switch] $IncludeDevelopment ) $normalized = String-WithoutWhitespace $Thumbprint $matches = @(Get-CodeSigningCertificates | Where-Object { (-not $normalized -or $_.Certificate.Thumbprint -eq $normalized) -and ($IncludeDevelopment -or -not $_.Development) }) if ($matches.Count -gt 0) { return $matches[0] } if ($normalized) { throw "No usable code-signing certificate with thumbprint $normalized and an accessible private key was found." } return $null } function String-WithoutWhitespace { param([AllowNull()] [string] $Value) return ([string]($Value -replace '\s', '')).ToUpperInvariant() } function Export-LocalDevelopmentCertificate { param([Parameter(Mandatory)] [System.Security.Cryptography.X509Certificates.X509Certificate2] $Certificate) $publicPath = $ExportDevelopmentCertificatePath if (-not $publicPath) { $publicPath = Join-Path ([IO.Path]::GetTempPath()) 'VehicleHelper-Development-CodeSigning.cer' } $publicBytes = $Certificate.Export([System.Security.Cryptography.X509Certificates.X509ContentType]::Cert) [IO.File]::WriteAllBytes($publicPath, $publicBytes) return $publicPath } function New-LocalDevelopmentCertificate { Write-Warning 'No release code-signing identity is installed. Creating a self-signed development certificate that is not publicly trusted.' $rsa = $null $generatedCertificate = $null $storedCertificate = $null $pfxBytes = $null try { # CertificateRequest avoids a known New-SelfSignedCertificate provider stall on # some unattended Windows hosts while still creating a non-exportable stored key. $rsa = [System.Security.Cryptography.RSA]::Create(3072) $request = [System.Security.Cryptography.X509Certificates.CertificateRequest]::new( $DevelopmentCertificateSubject, $rsa, [System.Security.Cryptography.HashAlgorithmName]::SHA256, [System.Security.Cryptography.RSASignaturePadding]::Pkcs1 ) $enhancedKeyUsages = [System.Security.Cryptography.OidCollection]::new() [void]$enhancedKeyUsages.Add([System.Security.Cryptography.Oid]::new('1.3.6.1.5.5.7.3.3')) $request.CertificateExtensions.Add( [System.Security.Cryptography.X509Certificates.X509EnhancedKeyUsageExtension]::new($enhancedKeyUsages, $false) ) $request.CertificateExtensions.Add( [System.Security.Cryptography.X509Certificates.X509KeyUsageExtension]::new( [System.Security.Cryptography.X509Certificates.X509KeyUsageFlags]::DigitalSignature, $true ) ) $request.CertificateExtensions.Add( [System.Security.Cryptography.X509Certificates.X509BasicConstraintsExtension]::new($false, $false, 0, $true) ) $request.CertificateExtensions.Add( [System.Security.Cryptography.X509Certificates.X509SubjectKeyIdentifierExtension]::new($request.PublicKey, $false) ) $generatedCertificate = $request.CreateSelfSigned( [DateTimeOffset]::Now.AddMinutes(-5), [DateTimeOffset]::Now.AddYears(1) ) $temporaryPassword = [Guid]::NewGuid().ToString('N') + [Guid]::NewGuid().ToString('N') $pfxBytes = $generatedCertificate.Export( [System.Security.Cryptography.X509Certificates.X509ContentType]::Pfx, $temporaryPassword ) $keyStorageFlags = [System.Security.Cryptography.X509Certificates.X509KeyStorageFlags]::UserKeySet -bor [System.Security.Cryptography.X509Certificates.X509KeyStorageFlags]::PersistKeySet $storedCertificate = [System.Security.Cryptography.X509Certificates.X509Certificate2]::new( $pfxBytes, $temporaryPassword, $keyStorageFlags ) $personalStore = [System.Security.Cryptography.X509Certificates.X509Store]::new('My', 'CurrentUser') try { $personalStore.Open([System.Security.Cryptography.X509Certificates.OpenFlags]::ReadWrite) $personalStore.Add($storedCertificate) } finally { $personalStore.Close() } $publicPath = Export-LocalDevelopmentCertificate -Certificate $storedCertificate Write-Warning "The development certificate was exported to $publicPath. Windows will report Unknown Publisher unless an administrator deliberately trusts that certificate." return [pscustomobject]@{ Certificate = $storedCertificate; Scope = 'CurrentUser'; Development = $true; PublicCertificatePath = $publicPath } } finally { if ($pfxBytes) { [Array]::Clear($pfxBytes, 0, $pfxBytes.Length) } if ($generatedCertificate) { $generatedCertificate.Dispose() } if ($rsa) { $rsa.Dispose() } } } function Invoke-SignTool { param( [Parameter(Mandatory)] [string] $Tool, [Parameter(Mandatory)] [string[]] $Arguments, [Parameter(Mandatory)] [string] $Operation, [switch] $AllowNonzeroExit ) $previousErrorAction = $ErrorActionPreference try { # Windows PowerShell 5.1 turns native stderr into ErrorRecord objects. Capture # it without letting a verification trust warning abort before we inspect it. $ErrorActionPreference = 'Continue' $nativeOutput = @(& $Tool @Arguments 2>&1) $exitCode = $LASTEXITCODE } finally { $ErrorActionPreference = $previousErrorAction } foreach ($line in $nativeOutput) { Write-Host ([string]$line) } if ($exitCode -ne 0 -and -not $AllowNonzeroExit) { throw "SignTool $Operation failed with exit code $exitCode." } return $exitCode } $signTool = Resolve-SignTool $resolvedMetadata = if ($PSCmdlet.ParameterSetName -eq 'ArtifactSigning') { (Resolve-Path -LiteralPath $MetadataPath).Path } else { '' } $resolvedDlib = if ($PSCmdlet.ParameterSetName -eq 'ArtifactSigning') { (Resolve-Path -LiteralPath $DlibPath).Path } else { '' } $signingCertificate = $null if ($PSCmdlet.ParameterSetName -ne 'ArtifactSigning') { $signingCertificate = Resolve-CodeSigningCertificate ` -Thumbprint $CertificateThumbprint ` -IncludeDevelopment:([bool]$CertificateThumbprint) if (-not $signingCertificate -and $AllowDevelopmentCertificate) { $signingCertificate = Resolve-CodeSigningCertificate -IncludeDevelopment if ($signingCertificate) { $publicPath = Export-LocalDevelopmentCertificate -Certificate $signingCertificate.Certificate $signingCertificate.PublicCertificatePath = $publicPath Write-Warning "Reusing the existing self-signed development certificate. Windows will report Unknown Publisher unless an administrator deliberately trusts $publicPath." } else { $signingCertificate = New-LocalDevelopmentCertificate } } if (-not $signingCertificate) { throw 'No usable code-signing certificate is installed. Install an organizational certificate, pass Artifact Signing metadata/Dlib, or explicitly use -AllowDevelopmentCertificate for local-only trust.' } if ($signingCertificate.Development -and -not $signingCertificate.PublicCertificatePath) { $signingCertificate.PublicCertificatePath = Export-LocalDevelopmentCertificate -Certificate $signingCertificate.Certificate } } $normalizedThumbprint = if ($signingCertificate) { String-WithoutWhitespace $signingCertificate.Certificate.Thumbprint } else { '' } foreach ($inputPath in $BinaryPath) { $resolvedBinary = (Resolve-Path -LiteralPath $inputPath -ErrorAction Stop).Path $extension = [IO.Path]::GetExtension($resolvedBinary).ToLowerInvariant() if ($extension -notin @('.exe', '.dll', '.msi', '.msix')) { throw "Unsupported Windows signing target: $resolvedBinary" } $signArguments = @( 'sign', '/v', '/fd', 'SHA256', '/tr', $TimestampUrl, '/td', 'SHA256', '/d', $Description ) if ($PSCmdlet.ParameterSetName -eq 'ArtifactSigning') { $signArguments += @('/debug', '/dlib', $resolvedDlib, '/dmdf', $resolvedMetadata) } else { if ($signingCertificate.Scope -eq 'LocalMachine') { $signArguments += '/sm' } $signArguments += @('/s', 'My', '/sha1', $normalizedThumbprint) } $signArguments += $resolvedBinary Write-Host "Signing $resolvedBinary" [void](Invoke-SignTool -Tool $signTool -Arguments $signArguments -Operation 'sign') $allowDevelopmentTrustFailure = $signingCertificate -and $signingCertificate.Development $verifyExitCode = Invoke-SignTool ` -Tool $signTool ` -Arguments @('verify', '/pa', '/all', '/v', $resolvedBinary) ` -Operation 'verify' ` -AllowNonzeroExit:$allowDevelopmentTrustFailure $signature = Get-AuthenticodeSignature -LiteralPath $resolvedBinary if (-not $signature.SignerCertificate) { throw "No signer certificate was found on $resolvedBinary." } if ($signingCertificate -and (String-WithoutWhitespace $signature.SignerCertificate.Thumbprint) -ne $normalizedThumbprint) { throw "The verified signer thumbprint did not match the selected certificate for $resolvedBinary." } if (-not $signature.TimeStamperCertificate) { throw "No RFC 3161 timestamp certificate was found on $resolvedBinary." } $developmentTrustStatus = $allowDevelopmentTrustFailure -and $signature.Status -in @( [System.Management.Automation.SignatureStatus]::UnknownError, [System.Management.Automation.SignatureStatus]::NotTrusted ) -and $signature.StatusMessage -match 'root certificate.+not trusted|certificate chain.+not trusted' if ($signature.Status -ne [System.Management.Automation.SignatureStatus]::Valid -and -not $developmentTrustStatus) { throw "Authenticode verification returned $($signature.Status) for ${resolvedBinary}: $($signature.StatusMessage)" } if ($verifyExitCode -ne 0 -and -not $developmentTrustStatus) { throw "SignTool verification failed with exit code $verifyExitCode for $resolvedBinary." } if ($developmentTrustStatus) { Write-Warning 'The exact signer and timestamp are present and the file hash is intact, but Windows does not trust the self-signed development root.' } Write-Host "Verified publisher: $($signature.SignerCertificate.Subject)" Write-Host "Signer thumbprint: $($signature.SignerCertificate.Thumbprint)" Write-Host "Trust scope: $(if ($allowDevelopmentTrustFailure) { 'self-signed development only' } else { 'release certificate' })" Write-Host "SHA-256: $((Get-FileHash -Algorithm SHA256 -LiteralPath $resolvedBinary).Hash)" } Write-Host "Signed and verified $($BinaryPath.Count) Windows artifact(s)."