helpers/certificates/CertificateHelpers.ps1
|
# ============================================================================= # Certificate Discovery Helpers for WhatsUpGoldPS # Scans IP addresses across multiple TCP ports to discover TLS certificates # and produce dashboard-ready data - similar to the F5 BIG-IP helpers. # # No additional modules required - uses .NET TcpClient + SslStream directly. # # Typical workflow: # 1. Get-CertificateInfo (discovers certs across IPs + ports) # 2. Get-CertificateDashboard (enriches with WUG device data, computes expiry) # 3. Export-CertificateDashboardHtml (renders an HTML report from template) # ============================================================================= # --------------------------------------------------------------------------- # Get-CertificateInfo # --------------------------------------------------------------------------- function Get-CertificateInfo { <# .SYNOPSIS Discovers TLS certificates by connecting to IP:port combinations. .DESCRIPTION For each IP address and TCP port combination, opens a TLS connection, retrieves the remote certificate, and returns rich certificate metadata. Failures are logged as warnings and skipped gracefully. .PARAMETER IPAddresses Array of IP addresses or hostnames to scan. .PARAMETER TcpPorts Array of TCP ports to try for each address. Defaults to 443, 8443. .PARAMETER ConnectTimeoutMs TCP connection timeout in milliseconds. Defaults to 5000. .EXAMPLE Get-CertificateInfo -IPAddresses "10.0.0.1" # Scans 10.0.0.1 on default ports (443, 8443). .EXAMPLE Get-CertificateInfo -IPAddresses "10.0.0.1","10.0.0.2" -TcpPorts 443,8443,4443 # Scans two hosts across three ports (6 total connection attempts). .EXAMPLE Get-CertificateInfo -IPAddresses (Get-Content .\hosts.txt) -ConnectTimeoutMs 3000 # Reads IPs from a file and uses a 3-second timeout per connection. .EXAMPLE $certs = Get-CertificateInfo -IPAddresses "webserver01" -TcpPorts 443 $certs | Select-Object IPAddress, Port, Subject, ExpirationDate # Scan a single host and display key fields. .EXAMPLE # Pull device IPs from WhatsUp Gold and scan them $devices = Get-WUGDevice -View overview Get-CertificateInfo -IPAddresses $devices.networkAddress -TcpPorts 443,8443 #> [CmdletBinding()] param( [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [string[]]$IPAddresses, [Parameter()] [int[]]$TcpPorts = @(443, 8443), [Parameter()] [int]$ConnectTimeoutMs = 5000 ) $results = @() foreach ($IPAddress in $IPAddresses) { Write-Host "Processing: $IPAddress" -ForegroundColor Cyan foreach ($port in $TcpPorts) { $tcpClient = $null $sslStream = $null try { # Connect with timeout $tcpClient = [System.Net.Sockets.TcpClient]::new() $connectTask = $tcpClient.ConnectAsync($IPAddress, [int]$port) if (-not $connectTask.Wait($ConnectTimeoutMs)) { throw "Timeout after ${ConnectTimeoutMs}ms connecting to ${IPAddress}:${port}" } # SSL stream with permissive validation (read cert only) $sslStream = [System.Net.Security.SslStream]::new( $tcpClient.GetStream(), $false, { param($sender, $cert, $chain, $errors) $true } ) # Authenticate (TLS 1.2) $sslStream.AuthenticateAsClient( $IPAddress, $null, [System.Security.Authentication.SslProtocols]::Tls12, $false ) # Grab and normalize certificate to X509Certificate2 $remoteCert = $sslStream.RemoteCertificate if ($null -eq $remoteCert) { throw "Failed to retrieve certificate from ${IPAddress}:${port}" } $certificate = [System.Security.Cryptography.X509Certificates.X509Certificate2]::new($remoteCert) # Extract SAN (Subject Alternative Names) $sanExt = $certificate.Extensions | Where-Object { $_.Oid.FriendlyName -eq 'Subject Alternative Name' } $san = if ($sanExt) { $sanExt.Format(0) } else { "" } # Extract certificate information $certObj = [PSCustomObject][ordered]@{ IPAddress = $IPAddress Port = [int]$port Subject = $certificate.Subject Issuer = $certificate.Issuer ExpirationDate = $certificate.NotAfter EffectiveDate = $certificate.NotBefore Thumbprint = $certificate.Thumbprint SerialNumber = $certificate.SerialNumber SAN = $san KeyAlgorithm = $certificate.GetKeyAlgorithm() KeyLength = if ($certificate.PublicKey.Key) { try { $certificate.PublicKey.Key.KeySize } catch { 0 } } else { 0 } SignatureAlgorithm = $certificate.SignatureAlgorithm.FriendlyName Version = $certificate.Version Format = $certificate.GetFormat() Extensions = if ($certificate.Extensions) { ($certificate.Extensions | ForEach-Object { "$($_.Oid.FriendlyName): $($_.Format(0))" }) -join '; ' } else { "None" } } $results += $certObj Write-Host " Found cert on port ${port}: $($certificate.Subject)" -ForegroundColor Green } catch { Write-Warning " Failed ${IPAddress}:${port} - $($_.Exception.Message)" } finally { if ($sslStream) { $sslStream.Dispose() } if ($tcpClient) { $tcpClient.Dispose() } } } } return $results } # --------------------------------------------------------------------------- # Get-CertificateDashboard # --------------------------------------------------------------------------- function Get-CertificateDashboard { <# .SYNOPSIS Builds enriched dashboard data from raw certificate scan results. .DESCRIPTION Takes the output of Get-CertificateInfo, computes days until expiration, assigns a health status (Critical/Warning/Healthy/Unknown), and optionally enriches each row with WhatsUp Gold device data if connected. .PARAMETER CertificateData Array of objects from Get-CertificateInfo. .PARAMETER WarningDays Certificates expiring within this many days are flagged as Warning. Defaults to 90. .PARAMETER CriticalDays Certificates expiring within this many days are flagged as Critical. Defaults to 30. .EXAMPLE $certs = Get-CertificateInfo -IPAddresses "10.0.0.1" -TcpPorts 443 Get-CertificateDashboard -CertificateData $certs # Enriches raw cert data with default thresholds (Warning: 90d, Critical: 30d). .EXAMPLE $certs = Get-CertificateInfo -IPAddresses "10.0.0.1","10.0.0.2" Get-CertificateDashboard -CertificateData $certs -WarningDays 60 -CriticalDays 14 # Uses custom thresholds: warn at 60 days, critical at 14 days. .EXAMPLE $certs = Get-CertificateInfo -IPAddresses "10.0.0.1" $dashboard = Get-CertificateDashboard -CertificateData $certs $dashboard | Where-Object { $_.Status -eq 'Critical' } | Format-Table IPAddress, Port, DaysUntilExpiry, Subject # Filter to only critical certificates and display as a table. .EXAMPLE # Full pipeline: scan -> enrich -> group by status $certs = Get-CertificateInfo -IPAddresses "10.0.0.1","10.0.0.2" -TcpPorts 443,8443 $dashboard = Get-CertificateDashboard -CertificateData $certs $dashboard | Group-Object Status | Select-Object Name, Count #> [CmdletBinding()] param( [Parameter(Mandatory)]$CertificateData, [int]$WarningDays = 90, [int]$CriticalDays = 30 ) # Build a WUG device index if connected $deviceIndex = @{} if ($global:WUGBearerHeaders) { try { $wugDevices = Get-WUGDevice -View overview foreach ($dev in $wugDevices) { if ($dev.networkAddress) { $deviceIndex[$dev.networkAddress] = $dev } } Write-Verbose "Indexed $($deviceIndex.Count) WUG devices for enrichment." } catch { Write-Warning "Could not retrieve WUG devices for enrichment: $($_.Exception.Message)" } } $dashboard = @() foreach ($cert in $CertificateData) { # Compute expiration $exp = $null try { $exp = [datetime]$cert.ExpirationDate } catch { $exp = $null } $daysUntilExpiry = if ($exp) { [int][math]::Floor(($exp - (Get-Date)).TotalDays) } else { [int]::MaxValue } # Determine status $status = if ($daysUntilExpiry -eq [int]::MaxValue) { "Unknown" } elseif ($daysUntilExpiry -lt 0) { "Expired" } elseif ($daysUntilExpiry -le $CriticalDays) { "Critical" } elseif ($daysUntilExpiry -le $WarningDays) { "Warning" } else { "Healthy" } # Self-signed check $selfSigned = if ($cert.Subject -eq $cert.Issuer) { "Yes" } else { "No" } # Build the row $row = [ordered]@{ IPAddress = $cert.IPAddress Port = $cert.Port Status = $status DaysUntilExpiry = if ($daysUntilExpiry -eq [int]::MaxValue) { "N/A" } else { $daysUntilExpiry } ExpirationDate = if ($exp) { $exp.ToString("yyyy-MM-dd HH:mm:ss") } else { "N/A" } EffectiveDate = if ($cert.EffectiveDate) { ([datetime]$cert.EffectiveDate).ToString("yyyy-MM-dd HH:mm:ss") } else { "N/A" } Subject = $cert.Subject Issuer = $cert.Issuer SelfSigned = $selfSigned SAN = $cert.SAN Thumbprint = $cert.Thumbprint SerialNumber = $cert.SerialNumber KeyAlgorithm = $cert.KeyAlgorithm KeyLength = $cert.KeyLength SignatureAlgorithm = $cert.SignatureAlgorithm Version = $cert.Version } # Enrich with WUG device data if available $dev = $null if ($cert.IPAddress -and $deviceIndex.ContainsKey($cert.IPAddress)) { $dev = $deviceIndex[$cert.IPAddress] } if ($dev) { $row["WUGDeviceId"] = $dev.id $row["WUGDeviceName"] = $dev.name $row["WUGHostName"] = $dev.hostName $row["WUGRole"] = $dev.role $row["WUGBrand"] = $dev.brand $row["WUGOS"] = $dev.os $row["WUGBestState"] = $dev.bestState $row["WUGWorstState"] = $dev.worstState $row["WUGActiveMonitors"] = $dev.totalActiveMonitors $row["WUGMonitorsDown"] = $dev.totalActiveMonitorsDown $row["WUGNotes"] = $dev.notes $row["WUGDescription"] = $dev.description } else { $row["WUGDeviceId"] = "" $row["WUGDeviceName"] = "" $row["WUGHostName"] = "" $row["WUGRole"] = "" $row["WUGBrand"] = "" $row["WUGOS"] = "" $row["WUGBestState"] = "" $row["WUGWorstState"] = "" $row["WUGActiveMonitors"] = "" $row["WUGMonitorsDown"] = "" $row["WUGNotes"] = "" $row["WUGDescription"] = "" } $dashboard += [PSCustomObject]$row } return $dashboard } # --------------------------------------------------------------------------- # Export-CertificateDashboardHtml # --------------------------------------------------------------------------- function Export-CertificateDashboardHtml { <# .SYNOPSIS Renders certificate dashboard data into a self-contained HTML file. .DESCRIPTION Takes the output of Get-CertificateDashboard and generates a Bootstrap-based HTML report with sortable, searchable, and exportable tables. Uses colour-coded status indicators for certificate expiration health. .PARAMETER DashboardData Array of objects from Get-CertificateDashboard. .PARAMETER OutputPath File path for the output HTML file. .PARAMETER ReportTitle Title shown in the report header. Defaults to "Certificate Dashboard". .PARAMETER TemplatePath Optional path to a custom HTML template. If omitted, uses the built-in template at helpers/certificates/Certificate-Dashboard-Template.html. .EXAMPLE $certs = Get-CertificateInfo -IPAddresses "10.0.0.1" -TcpPorts 443 $dashboard = Get-CertificateDashboard -CertificateData $certs Export-CertificateDashboardHtml -DashboardData $dashboard -OutputPath "C:\Reports\certs.html" # Generates an HTML dashboard at the specified path. .EXAMPLE $certs = Get-CertificateInfo -IPAddresses "10.0.0.1","10.0.0.2" $dashboard = Get-CertificateDashboard -CertificateData $certs Export-CertificateDashboardHtml -DashboardData $dashboard -OutputPath "$env:TEMP\certs.html" -ReportTitle "Prod Certificate Audit" # Custom report title. .EXAMPLE # Full end-to-end: scan -> enrich -> HTML report -> open in browser $certs = Get-CertificateInfo -IPAddresses (Get-Content .\hosts.txt) -TcpPorts 443,8443 $dashboard = Get-CertificateDashboard -CertificateData $certs $outPath = "$env:TEMP\Certificate-Dashboard.html" Export-CertificateDashboardHtml -DashboardData $dashboard -OutputPath $outPath Start-Process $outPath #> [CmdletBinding()] param( [Parameter(Mandatory)]$DashboardData, [Parameter(Mandatory)][string]$OutputPath, [string]$ReportTitle = "Certificate Dashboard", [string]$TemplatePath ) if (-not $TemplatePath) { $TemplatePath = Join-Path $PSScriptRoot "Certificate-Dashboard-Template.html" } if (-not (Test-Path $TemplatePath)) { throw "HTML template not found at $TemplatePath" } # Build column definitions for bootstrap-table $firstObj = $DashboardData | Select-Object -First 1 $columns = @() foreach ($prop in $firstObj.PSObject.Properties) { $col = @{ field = $prop.Name title = ($prop.Name -creplace '([A-Z])', ' $1').Trim() sortable = $true searchable = $true } # Apply formatters for status columns if ($prop.Name -eq 'Status') { $col.formatter = 'formatCertStatus' } if ($prop.Name -eq 'DaysUntilExpiry') { $col.formatter = 'formatDaysUntilExpiry' } if ($prop.Name -match 'WUGBestState|WUGWorstState') { $col.formatter = 'formatWugState' } $columns += $col } $columnsJson = $columns | ConvertTo-Json -Depth 5 -Compress $dataJson = $DashboardData | ConvertTo-Json -Depth 5 -Compress $tableConfig = @" columns: $columnsJson, data: $dataJson "@ $html = Get-Content -Path $TemplatePath -Raw $html = $html -replace 'replaceThisHere', $tableConfig $html = $html -replace 'ReplaceYourReportNameHere', $ReportTitle $html = $html -replace 'ReplaceUpdateTimeHere', (Get-Date).ToString("yyyy-MM-dd HH:mm:ss") Set-Content -Path $OutputPath -Value $html -Encoding UTF8 Write-Verbose "Certificate Dashboard HTML written to $OutputPath" } # SIG # Begin signature block # MIIVlwYJKoZIhvcNAQcCoIIViDCCFYQCAQExDzANBglghkgBZQMEAgEFADB5Bgor # BgEEAYI3AgEEoGswaTA0BgorBgEEAYI3AgEeMCYCAwEAAAQQH8w7YFlLCE63JNLG # KX7zUQIBAAIBAAIBAAIBAAIBADAxMA0GCWCGSAFlAwQCAQUABCDq+lPZnKo1RRAA # kJLWd3mhcg7Ilr6BadrzTpuMIFN4YaCCEdMwggVvMIIEV6ADAgECAhBI/JO0YFWU # jTanyYqJ1pQWMA0GCSqGSIb3DQEBDAUAMHsxCzAJBgNVBAYTAkdCMRswGQYDVQQI # DBJHcmVhdGVyIE1hbmNoZXN0ZXIxEDAOBgNVBAcMB1NhbGZvcmQxGjAYBgNVBAoM # EUNvbW9kbyBDQSBMaW1pdGVkMSEwHwYDVQQDDBhBQUEgQ2VydGlmaWNhdGUgU2Vy # dmljZXMwHhcNMjEwNTI1MDAwMDAwWhcNMjgxMjMxMjM1OTU5WjBWMQswCQYDVQQG # EwJHQjEYMBYGA1UEChMPU2VjdGlnbyBMaW1pdGVkMS0wKwYDVQQDEyRTZWN0aWdv # IFB1YmxpYyBDb2RlIFNpZ25pbmcgUm9vdCBSNDYwggIiMA0GCSqGSIb3DQEBAQUA # A4ICDwAwggIKAoICAQCN55QSIgQkdC7/FiMCkoq2rjaFrEfUI5ErPtx94jGgUW+s # hJHjUoq14pbe0IdjJImK/+8Skzt9u7aKvb0Ffyeba2XTpQxpsbxJOZrxbW6q5KCD # J9qaDStQ6Utbs7hkNqR+Sj2pcaths3OzPAsM79szV+W+NDfjlxtd/R8SPYIDdub7 # P2bSlDFp+m2zNKzBenjcklDyZMeqLQSrw2rq4C+np9xu1+j/2iGrQL+57g2extme # me/G3h+pDHazJyCh1rr9gOcB0u/rgimVcI3/uxXP/tEPNqIuTzKQdEZrRzUTdwUz # T2MuuC3hv2WnBGsY2HH6zAjybYmZELGt2z4s5KoYsMYHAXVn3m3pY2MeNn9pib6q # RT5uWl+PoVvLnTCGMOgDs0DGDQ84zWeoU4j6uDBl+m/H5x2xg3RpPqzEaDux5mcz # mrYI4IAFSEDu9oJkRqj1c7AGlfJsZZ+/VVscnFcax3hGfHCqlBuCF6yH6bbJDoEc # QNYWFyn8XJwYK+pF9e+91WdPKF4F7pBMeufG9ND8+s0+MkYTIDaKBOq3qgdGnA2T # OglmmVhcKaO5DKYwODzQRjY1fJy67sPV+Qp2+n4FG0DKkjXp1XrRtX8ArqmQqsV/ # AZwQsRb8zG4Y3G9i/qZQp7h7uJ0VP/4gDHXIIloTlRmQAOka1cKG8eOO7F/05QID # AQABo4IBEjCCAQ4wHwYDVR0jBBgwFoAUoBEKIz6W8Qfs4q8p74Klf9AwpLQwHQYD # VR0OBBYEFDLrkpr/NZZILyhAQnAgNpFcF4XmMA4GA1UdDwEB/wQEAwIBhjAPBgNV # HRMBAf8EBTADAQH/MBMGA1UdJQQMMAoGCCsGAQUFBwMDMBsGA1UdIAQUMBIwBgYE # VR0gADAIBgZngQwBBAEwQwYDVR0fBDwwOjA4oDagNIYyaHR0cDovL2NybC5jb21v # ZG9jYS5jb20vQUFBQ2VydGlmaWNhdGVTZXJ2aWNlcy5jcmwwNAYIKwYBBQUHAQEE # KDAmMCQGCCsGAQUFBzABhhhodHRwOi8vb2NzcC5jb21vZG9jYS5jb20wDQYJKoZI # hvcNAQEMBQADggEBABK/oe+LdJqYRLhpRrWrJAoMpIpnuDqBv0WKfVIHqI0fTiGF # OaNrXi0ghr8QuK55O1PNtPvYRL4G2VxjZ9RAFodEhnIq1jIV9RKDwvnhXRFAZ/ZC # J3LFI+ICOBpMIOLbAffNRk8monxmwFE2tokCVMf8WPtsAO7+mKYulaEMUykfb9gZ # pk+e96wJ6l2CxouvgKe9gUhShDHaMuwV5KZMPWw5c9QLhTkg4IUaaOGnSDip0TYl # d8GNGRbFiExmfS9jzpjoad+sPKhdnckcW67Y8y90z7h+9teDnRGWYpquRRPaf9xH # +9/DUp/mBlXpnYzyOmJRvOwkDynUWICE5EV7WtgwggYaMIIEAqADAgECAhBiHW0M # UgGeO5B5FSCJIRwKMA0GCSqGSIb3DQEBDAUAMFYxCzAJBgNVBAYTAkdCMRgwFgYD # VQQKEw9TZWN0aWdvIExpbWl0ZWQxLTArBgNVBAMTJFNlY3RpZ28gUHVibGljIENv # ZGUgU2lnbmluZyBSb290IFI0NjAeFw0yMTAzMjIwMDAwMDBaFw0zNjAzMjEyMzU5 # NTlaMFQxCzAJBgNVBAYTAkdCMRgwFgYDVQQKEw9TZWN0aWdvIExpbWl0ZWQxKzAp # BgNVBAMTIlNlY3RpZ28gUHVibGljIENvZGUgU2lnbmluZyBDQSBSMzYwggGiMA0G # CSqGSIb3DQEBAQUAA4IBjwAwggGKAoIBgQCbK51T+jU/jmAGQ2rAz/V/9shTUxjI # ztNsfvxYB5UXeWUzCxEeAEZGbEN4QMgCsJLZUKhWThj/yPqy0iSZhXkZ6Pg2A2NV # DgFigOMYzB2OKhdqfWGVoYW3haT29PSTahYkwmMv0b/83nbeECbiMXhSOtbam+/3 # 6F09fy1tsB8je/RV0mIk8XL/tfCK6cPuYHE215wzrK0h1SWHTxPbPuYkRdkP05Zw # mRmTnAO5/arnY83jeNzhP06ShdnRqtZlV59+8yv+KIhE5ILMqgOZYAENHNX9SJDm # +qxp4VqpB3MV/h53yl41aHU5pledi9lCBbH9JeIkNFICiVHNkRmq4TpxtwfvjsUe # dyz8rNyfQJy/aOs5b4s+ac7IH60B+Ja7TVM+EKv1WuTGwcLmoU3FpOFMbmPj8pz4 # 4MPZ1f9+YEQIQty/NQd/2yGgW+ufflcZ/ZE9o1M7a5Jnqf2i2/uMSWymR8r2oQBM # dlyh2n5HirY4jKnFH/9gRvd+QOfdRrJZb1sCAwEAAaOCAWQwggFgMB8GA1UdIwQY # MBaAFDLrkpr/NZZILyhAQnAgNpFcF4XmMB0GA1UdDgQWBBQPKssghyi47G9IritU # pimqF6TNDDAOBgNVHQ8BAf8EBAMCAYYwEgYDVR0TAQH/BAgwBgEB/wIBADATBgNV # HSUEDDAKBggrBgEFBQcDAzAbBgNVHSAEFDASMAYGBFUdIAAwCAYGZ4EMAQQBMEsG # A1UdHwREMEIwQKA+oDyGOmh0dHA6Ly9jcmwuc2VjdGlnby5jb20vU2VjdGlnb1B1 # YmxpY0NvZGVTaWduaW5nUm9vdFI0Ni5jcmwwewYIKwYBBQUHAQEEbzBtMEYGCCsG # AQUFBzAChjpodHRwOi8vY3J0LnNlY3RpZ28uY29tL1NlY3RpZ29QdWJsaWNDb2Rl # U2lnbmluZ1Jvb3RSNDYucDdjMCMGCCsGAQUFBzABhhdodHRwOi8vb2NzcC5zZWN0 # aWdvLmNvbTANBgkqhkiG9w0BAQwFAAOCAgEABv+C4XdjNm57oRUgmxP/BP6YdURh # w1aVcdGRP4Wh60BAscjW4HL9hcpkOTz5jUug2oeunbYAowbFC2AKK+cMcXIBD0Zd # OaWTsyNyBBsMLHqafvIhrCymlaS98+QpoBCyKppP0OcxYEdU0hpsaqBBIZOtBajj # cw5+w/KeFvPYfLF/ldYpmlG+vd0xqlqd099iChnyIMvY5HexjO2AmtsbpVn0OhNc # WbWDRF/3sBp6fWXhz7DcML4iTAWS+MVXeNLj1lJziVKEoroGs9Mlizg0bUMbOalO # hOfCipnx8CaLZeVme5yELg09Jlo8BMe80jO37PU8ejfkP9/uPak7VLwELKxAMcJs # zkyeiaerlphwoKx1uHRzNyE6bxuSKcutisqmKL5OTunAvtONEoteSiabkPVSZ2z7 # 6mKnzAfZxCl/3dq3dUNw4rg3sTCggkHSRqTqlLMS7gjrhTqBmzu1L90Y1KWN/Y5J # KdGvspbOrTfOXyXvmPL6E52z1NZJ6ctuMFBQZH3pwWvqURR8AgQdULUvrxjUYbHH # j95Ejza63zdrEcxWLDX6xWls/GDnVNueKjWUH3fTv1Y8Wdho698YADR7TNx8X8z2 # Bev6SivBBOHY+uqiirZtg0y9ShQoPzmCcn63Syatatvx157YK9hlcPmVoa1oDE5/ # L9Uo2bC5a4CH2RwwggY+MIIEpqADAgECAhAHnODk0RR/hc05c892LTfrMA0GCSqG # SIb3DQEBDAUAMFQxCzAJBgNVBAYTAkdCMRgwFgYDVQQKEw9TZWN0aWdvIExpbWl0 # ZWQxKzApBgNVBAMTIlNlY3RpZ28gUHVibGljIENvZGUgU2lnbmluZyBDQSBSMzYw # HhcNMjYwMjA5MDAwMDAwWhcNMjkwNDIxMjM1OTU5WjBVMQswCQYDVQQGEwJVUzEU # MBIGA1UECAwLQ29ubmVjdGljdXQxFzAVBgNVBAoMDkphc29uIEFsYmVyaW5vMRcw # FQYDVQQDDA5KYXNvbiBBbGJlcmlubzCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCC # AgoCggIBAPN6aN4B1yYWkI5b5TBj3I0VV/peETrHb6EY4BHGxt8Ap+eT+WpEpJyE # tRYPxEmNJL3A38Bkg7mwzPE3/1NK570ZBCuBjSAn4mSDIgIuXZnvyBO9W1OQs5d6 # 7MlJLUAEufl18tOr3ST1DeO9gSjQSAE5Nql0QDxPnm93OZBon+Fz3CmE+z3MwAe2 # h4KdtRAnCqwM+/V7iBdbw+JOxolpx+7RVjGyProTENIG3pe/hKvPb501lf8uBAAD # LdjZr5ip8vIWbf857Yw1Bu10nVI7HW3eE8Cl5//d1ribHlzTzQLfttW+k+DaFsKZ # BBL56l4YAlIVRsrOiE1kdHYYx6IGrEA809R7+TZA9DzGqyFiv9qmJAbL4fDwetDe # yIq+Oztz1LvEdy8Rcd0JBY+J4S0eDEFIA3X0N8VcLeAwabKb9AjulKXwUeqCJLvN # 79CJ90UTZb2+I+tamj0dn+IKMEsJ4v4Ggx72sxFr9+6XziodtTg5Luf2xd6+Phha # mOxF2px9LObhBLLEMyRsCHZIzVZOFKu9BpHQH7ufGB+Sa80Tli0/6LEyn9+bMYWi # 2ttn6lLOPThXMiQaooRUq6q2u3+F4SaPlxVFLI7OJVMhar6nW6joBvELTJPmANSM # jDSRFDfHRCdGbZsL/keELJNy+jZctF6VvxQEjFM8/bazu6qYhrA7AgMBAAGjggGJ # MIIBhTAfBgNVHSMEGDAWgBQPKssghyi47G9IritUpimqF6TNDDAdBgNVHQ4EFgQU # 6YF0o0D5AVhKHbVocr8GaSIBibAwDgYDVR0PAQH/BAQDAgeAMAwGA1UdEwEB/wQC # MAAwEwYDVR0lBAwwCgYIKwYBBQUHAwMwSgYDVR0gBEMwQTA1BgwrBgEEAbIxAQIB # AwIwJTAjBggrBgEFBQcCARYXaHR0cHM6Ly9zZWN0aWdvLmNvbS9DUFMwCAYGZ4EM # AQQBMEkGA1UdHwRCMEAwPqA8oDqGOGh0dHA6Ly9jcmwuc2VjdGlnby5jb20vU2Vj # dGlnb1B1YmxpY0NvZGVTaWduaW5nQ0FSMzYuY3JsMHkGCCsGAQUFBwEBBG0wazBE # BggrBgEFBQcwAoY4aHR0cDovL2NydC5zZWN0aWdvLmNvbS9TZWN0aWdvUHVibGlj # Q29kZVNpZ25pbmdDQVIzNi5jcnQwIwYIKwYBBQUHMAGGF2h0dHA6Ly9vY3NwLnNl # Y3RpZ28uY29tMA0GCSqGSIb3DQEBDAUAA4IBgQAEIsm4xnOd/tZMVrKwi3doAXvC # wOA/RYQnFJD7R/bSQRu3wXEK4o9SIefye18B/q4fhBkhNAJuEvTQAGfqbbpxow03 # J5PrDTp1WPCWbXKX8Oz9vGWJFyJxRGftkdzZ57JE00synEMS8XCwLO9P32MyR9Z9 # URrpiLPJ9rQjfHMb1BUdvaNayomm7aWLAnD+X7jm6o8sNT5An1cwEAob7obWDM6s # X93wphwJNBJAstH9Ozs6LwISOX6sKS7CKm9N3Kp8hOUue0ZHAtZdFl6o5u12wy+z # zieGEI50fKnN77FfNKFOWKlS6OJwlArcbFegB5K89LcE5iNSmaM3VMB2ADV1FEcj # GSHw4lTg1Wx+WMAMdl/7nbvfFxJ9uu5tNiT54B0s+lZO/HztwXYQUczdsFon3pjs # Nrsk9ZlalBi5SHkIu+F6g7tWiEv3rtVApmJRnLkUr2Xq2a4nbslUCt4jKs5UX4V1 # nSX8OM++AXoyVGO+iTj7z+pl6XE9Gw/Td6WKKKsxggMaMIIDFgIBATBoMFQxCzAJ # BgNVBAYTAkdCMRgwFgYDVQQKEw9TZWN0aWdvIExpbWl0ZWQxKzApBgNVBAMTIlNl # Y3RpZ28gUHVibGljIENvZGUgU2lnbmluZyBDQSBSMzYCEAec4OTRFH+FzTlzz3Yt # N+swDQYJYIZIAWUDBAIBBQCggYQwGAYKKwYBBAGCNwIBDDEKMAigAoAAoQKAADAZ # BgkqhkiG9w0BCQMxDAYKKwYBBAGCNwIBBDAcBgorBgEEAYI3AgELMQ4wDAYKKwYB # BAGCNwIBFTAvBgkqhkiG9w0BCQQxIgQgbNNqEWWC/ueT8sLRURyPKNmGgVzRt6Dt # cPUo4ihPxRQwDQYJKoZIhvcNAQEBBQAEggIAmtZZvf22uxWdyxJKJ2B2LgOoC9wf # s/HizsOJw2X1MY/1yzZQaB5HSOB/CXtmKB0zikpUS2m+ri3Fi0t6716YWtfTmINE # yg1BnSP8TZymnTmLqGJkV3jupMQxL40k91ewP5zb6wrkM3DqcxAUgDEGeEG8Z5xb # I0JsUa086p4MIAJm9Sc3v6UPNt8u9jrbWrLMOpeSO129KiBmJHfBzGvo1RRzcoFM # UGpLas6DcTAyIcUxjDSmqriMdJDTC2E17o9DkCpDsNqNZ8SYqi3s/PYcMy4ph5Tp # DfV7uNWJZjljL4Af9DqU6jFbtYWIArXZ2ozphQawzBwk9V+4b+tbWSucsEVTrTAS # vOGm5DdtLtKeXnNzu5IUsL3KHW5FXBOOvw/ssVfzsDBW6DulBKoHAlRF36PHo9qj # 7qlAQlUQBQCQonXmoTDv729SeIBE8HhrmKjpG/M/jlV4wJu3sCjPTj415Q0i+46N # CwNaex5g/38aEnygks1VXzWwDfN7bryLlvZ7fW/k82E+FxMxYCmvMdQG01ZWcyAI # 2pqVOl7U+vjBKfAT1iTISWNIupxFSl7fZgQHDRB+5A6Pk6nKd59lgodpxdbfBUJj # +W7/notSaPO9FrNYmMXED6u2KfqUb56yzlRanRrpO5WpkWJNn51MOuDYLGmXWJvM # dJIAtfnQVmsyNnQ= # SIG # End signature block |