mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-09-10 10:35:51 +00:00
Run the Windows installer TLS callback off a compiled type
install.ps1 set ServicePointManager's certificate callback to a PowerShell scriptblock. ServicePointManager can invoke that callback on a worker thread that has no runspace, where a scriptblock delegate fails closed, so agent downloads died with a TLS error even though the certificate policy would have accepted them (hit by an rc.6 tester with Skip TLS verification enabled). The copied install command already migrated to a compiled validator for exactly this reason in rc.6; the downloaded script now does the same. Validation policy is unchanged, with fingerprint pinning first, then insecure, then clean chains, then the custom CA. Verified with pwsh covering all four policy branches plus the callback running on a runspace-free worker thread. Contract-Neutral: installer TLS callback implementation moved to a compiled type; same validation policy, no payload or endpoint change
This commit is contained in:
+103
-68
@@ -241,42 +241,6 @@ function Load-CustomCaCertificate {
|
||||
return [System.Security.Cryptography.X509Certificates.X509Certificate2]::new($resolvedPath)
|
||||
}
|
||||
|
||||
function Test-CertificateTrustedByCustomCa {
|
||||
param(
|
||||
[System.Security.Cryptography.X509Certificates.X509Certificate]$Certificate,
|
||||
[System.Security.Cryptography.X509Certificates.X509Certificate2]$CustomCaCertificate
|
||||
)
|
||||
|
||||
if ($null -eq $Certificate -or $null -eq $CustomCaCertificate) {
|
||||
return $false
|
||||
}
|
||||
|
||||
$leaf = if ($Certificate -is [System.Security.Cryptography.X509Certificates.X509Certificate2]) {
|
||||
$Certificate
|
||||
} else {
|
||||
[System.Security.Cryptography.X509Certificates.X509Certificate2]::new($Certificate)
|
||||
}
|
||||
|
||||
$chain = [System.Security.Cryptography.X509Certificates.X509Chain]::new()
|
||||
try {
|
||||
$chain.ChainPolicy.RevocationMode = [System.Security.Cryptography.X509Certificates.X509RevocationMode]::NoCheck
|
||||
$chain.ChainPolicy.VerificationFlags = [System.Security.Cryptography.X509Certificates.X509VerificationFlags]::AllowUnknownCertificateAuthority
|
||||
$null = $chain.ChainPolicy.ExtraStore.Add($CustomCaCertificate)
|
||||
if (-not $chain.Build($leaf)) {
|
||||
return $false
|
||||
}
|
||||
|
||||
foreach ($element in $chain.ChainElements) {
|
||||
if ($element.Certificate.Thumbprint -eq $CustomCaCertificate.Thumbprint) {
|
||||
return $true
|
||||
}
|
||||
}
|
||||
return $false
|
||||
} finally {
|
||||
$chain.Dispose()
|
||||
}
|
||||
}
|
||||
|
||||
function Test-PEBinary {
|
||||
param([string]$FilePath)
|
||||
if (-not (Test-Path $FilePath)) { return $false }
|
||||
@@ -370,6 +334,95 @@ function Invoke-InstallerSignatureVerification {
|
||||
Write-Host "Cryptographic signature verified." -ForegroundColor Green
|
||||
}
|
||||
|
||||
# ServicePointManager can invoke its validation callback on a worker thread
|
||||
# that has no PowerShell runspace, where a scriptblock delegate fails closed
|
||||
# and the download dies with a TLS error even though the certificate policy
|
||||
# would have accepted it. Keep the callback in a compiled .NET type that runs
|
||||
# on any thread, mirroring the copied install command's validator.
|
||||
$script:PulseTlsValidatorSource = @'
|
||||
using System;
|
||||
using System.Net.Security;
|
||||
using System.Security.Cryptography;
|
||||
using System.Security.Cryptography.X509Certificates;
|
||||
|
||||
public static class PulseAgentInstallTlsValidator
|
||||
{
|
||||
public static bool AllowInsecure;
|
||||
public static string PinnedSha256Fingerprint;
|
||||
public static X509Certificate2 CustomCa;
|
||||
public static readonly RemoteCertificateValidationCallback Callback = Validate;
|
||||
|
||||
private static bool Validate(object sender, X509Certificate certificate, X509Chain chain, SslPolicyErrors errors)
|
||||
{
|
||||
if (!String.IsNullOrEmpty(PinnedSha256Fingerprint))
|
||||
{
|
||||
if (certificate == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
try
|
||||
{
|
||||
using (SHA256 sha256 = SHA256.Create())
|
||||
{
|
||||
string actual = BitConverter.ToString(sha256.ComputeHash(certificate.GetRawCertData())).Replace("-", "");
|
||||
return String.Equals(actual, PinnedSha256Fingerprint, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (AllowInsecure)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (errors == SslPolicyErrors.None)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (certificate == null || CustomCa == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
X509Certificate2 leaf = certificate as X509Certificate2;
|
||||
if (leaf == null)
|
||||
{
|
||||
leaf = new X509Certificate2(certificate);
|
||||
}
|
||||
|
||||
using (X509Chain candidateChain = new X509Chain())
|
||||
{
|
||||
candidateChain.ChainPolicy.RevocationMode = X509RevocationMode.NoCheck;
|
||||
candidateChain.ChainPolicy.VerificationFlags = X509VerificationFlags.AllowUnknownCertificateAuthority;
|
||||
candidateChain.ChainPolicy.ExtraStore.Add(CustomCa);
|
||||
if (!candidateChain.Build(leaf))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
foreach (X509ChainElement element in candidateChain.ChainElements)
|
||||
{
|
||||
if (String.Equals(element.Certificate.Thumbprint, CustomCa.Thumbprint, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
'@
|
||||
|
||||
function Initialize-PulseTlsValidator {
|
||||
if ($null -eq ("PulseAgentInstallTlsValidator" -as [type])) {
|
||||
Add-Type -TypeDefinition $script:PulseTlsValidatorSource
|
||||
}
|
||||
}
|
||||
|
||||
function Invoke-WithOptionalInsecureTls {
|
||||
param(
|
||||
[bool]$AllowInsecure,
|
||||
@@ -377,43 +430,25 @@ function Invoke-WithOptionalInsecureTls {
|
||||
[scriptblock]$Action
|
||||
)
|
||||
|
||||
$previousCallback = [System.Net.ServicePointManager]::ServerCertificateValidationCallback
|
||||
if ($AllowInsecure -or $null -ne $CustomCaCertificate -or -not [string]::IsNullOrWhiteSpace($ServerFingerprint)) {
|
||||
[System.Net.ServicePointManager]::ServerCertificateValidationCallback = {
|
||||
param($sender, $certificate, $chain, $sslPolicyErrors)
|
||||
|
||||
if (-not [string]::IsNullOrWhiteSpace($ServerFingerprint)) {
|
||||
try {
|
||||
$sha256 = [System.Security.Cryptography.SHA256]::Create()
|
||||
try {
|
||||
$actualFingerprint = ([System.BitConverter]::ToString($sha256.ComputeHash($certificate.GetRawCertData()))).Replace('-', '').ToLowerInvariant()
|
||||
} finally {
|
||||
$sha256.Dispose()
|
||||
}
|
||||
return [string]::Equals($actualFingerprint, $ServerFingerprint, [System.StringComparison]::OrdinalIgnoreCase)
|
||||
} catch {
|
||||
return $false
|
||||
}
|
||||
}
|
||||
|
||||
if ($AllowInsecure) {
|
||||
return $true
|
||||
}
|
||||
|
||||
if ($sslPolicyErrors -eq [System.Net.Security.SslPolicyErrors]::None) {
|
||||
return $true
|
||||
}
|
||||
|
||||
return Test-CertificateTrustedByCustomCa -Certificate $certificate -CustomCaCertificate $CustomCaCertificate
|
||||
}
|
||||
$needsCallback = $AllowInsecure -or $null -ne $CustomCaCertificate -or -not [string]::IsNullOrWhiteSpace($ServerFingerprint)
|
||||
if (-not $needsCallback) {
|
||||
& $Action
|
||||
return
|
||||
}
|
||||
|
||||
Initialize-PulseTlsValidator
|
||||
$previousCallback = [System.Net.ServicePointManager]::ServerCertificateValidationCallback
|
||||
[PulseAgentInstallTlsValidator]::AllowInsecure = $AllowInsecure
|
||||
[PulseAgentInstallTlsValidator]::PinnedSha256Fingerprint = $ServerFingerprint
|
||||
[PulseAgentInstallTlsValidator]::CustomCa = $CustomCaCertificate
|
||||
[System.Net.ServicePointManager]::ServerCertificateValidationCallback = [PulseAgentInstallTlsValidator]::Callback
|
||||
try {
|
||||
& $Action
|
||||
} finally {
|
||||
if ($AllowInsecure -or $null -ne $CustomCaCertificate -or -not [string]::IsNullOrWhiteSpace($ServerFingerprint)) {
|
||||
[System.Net.ServicePointManager]::ServerCertificateValidationCallback = $previousCallback
|
||||
}
|
||||
[System.Net.ServicePointManager]::ServerCertificateValidationCallback = $previousCallback
|
||||
[PulseAgentInstallTlsValidator]::AllowInsecure = $false
|
||||
[PulseAgentInstallTlsValidator]::PinnedSha256Fingerprint = $null
|
||||
[PulseAgentInstallTlsValidator]::CustomCa = $null
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -195,7 +195,7 @@ func TestInstallPS1PersistsAndVerifiesServerFingerprint(t *testing.T) {
|
||||
script := string(content)
|
||||
required := []string{
|
||||
`[string]$ServerFingerprint = $env:PULSE_SERVER_FINGERPRINT,`,
|
||||
`$sha256.ComputeHash($certificate.GetRawCertData())`,
|
||||
`sha256.ComputeHash(certificate.GetRawCertData())`,
|
||||
`$lines += "PULSE_SERVER_FINGERPRINT='$ServerFingerprint'"`,
|
||||
`$ServerFingerprint = Get-ConnectionStateValue "PULSE_SERVER_FINGERPRINT"`,
|
||||
`$ServiceArgs += @("--server-fingerprint", "` + "`" + `"$ServerFingerprint` + "`" + `"")`,
|
||||
@@ -346,9 +346,12 @@ func TestInstallPS1UsesInsecureTlsForRuntimeTransport(t *testing.T) {
|
||||
script := string(content)
|
||||
required := []string{
|
||||
`function Invoke-WithOptionalInsecureTls {`,
|
||||
`if ($AllowInsecure -or $null -ne $CustomCaCertificate -or -not [string]::IsNullOrWhiteSpace($ServerFingerprint)) {`,
|
||||
`if ($AllowInsecure) {`,
|
||||
`return Test-CertificateTrustedByCustomCa -Certificate $certificate -CustomCaCertificate $CustomCaCertificate`,
|
||||
`$needsCallback = $AllowInsecure -or $null -ne $CustomCaCertificate -or -not [string]::IsNullOrWhiteSpace($ServerFingerprint)`,
|
||||
// The callback must be a compiled delegate, not a scriptblock:
|
||||
// ServicePointManager can invoke it on a worker thread with no
|
||||
// PowerShell runspace, where a scriptblock fails closed.
|
||||
`[System.Net.ServicePointManager]::ServerCertificateValidationCallback = [PulseAgentInstallTlsValidator]::Callback`,
|
||||
`[PulseAgentInstallTlsValidator]::AllowInsecure = $AllowInsecure`,
|
||||
`if ($Url.ToLowerInvariant().StartsWith("http://") -and -not $Insecure) {`,
|
||||
`Plain HTTP Pulse URL detected; enabling insecure mode for persisted agent update checks.`,
|
||||
`Invoke-WithOptionalInsecureTls -AllowInsecure $Insecure -CustomCaCertificate $CustomCaCertificate -Action {`,
|
||||
@@ -479,8 +482,8 @@ func TestInstallPS1SupportsCustomCATransport(t *testing.T) {
|
||||
`[string]$CACertPath = $env:PULSE_CACERT,`,
|
||||
`function Load-CustomCaCertificate {`,
|
||||
`return [System.Security.Cryptography.X509Certificates.X509Certificate2]::new($resolvedPath)`,
|
||||
`function Test-CertificateTrustedByCustomCa {`,
|
||||
`$chain.ChainPolicy.ExtraStore.Add($CustomCaCertificate)`,
|
||||
`public static class PulseAgentInstallTlsValidator`,
|
||||
`candidateChain.ChainPolicy.ExtraStore.Add(CustomCa);`,
|
||||
`Invoke-WithOptionalInsecureTls -AllowInsecure $Insecure -CustomCaCertificate $CustomCaCertificate -Action {`,
|
||||
`Show-Error "Invalid CA certificate path. File does not exist.`,
|
||||
`Show-Error "Invalid CA certificate file. Provide a PEM, CRT, or CER certificate.`,
|
||||
|
||||
Reference in New Issue
Block a user