TLSleuth.psm1
#Region '.\private\Build-CertificateChain.ps1' -1 function Build-CertificateChain { <# .SYNOPSIS Builds a trust chain for a certificate with optional revocation checking. #> [CmdletBinding()] param( [Parameter(Mandatory)] [System.Security.Cryptography.X509Certificates.X509Certificate2]$Certificate, [switch]$CheckRevocation ) $fn = $MyInvocation.MyCommand.Name $sw = [System.Diagnostics.Stopwatch]::StartNew() Write-Verbose "[$fn] Begin (Revocation=$([bool]$CheckRevocation), Thumbprint=$($Certificate.Thumbprint))" try { $chain = [System.Security.Cryptography.X509Certificates.X509Chain]::new() $chain.ChainPolicy.RevocationMode = if ($CheckRevocation) { [System.Security.Cryptography.X509Certificates.X509RevocationMode]::Online } else { [System.Security.Cryptography.X509Certificates.X509RevocationMode]::NoCheck } $chain.ChainPolicy.RevocationFlag = [System.Security.Cryptography.X509Certificates.X509RevocationFlag]::EndCertificateOnly $chain.ChainPolicy.VerificationFlags = [System.Security.Cryptography.X509Certificates.X509VerificationFlags]::NoFlag $isTrusted = $chain.Build($Certificate) Write-Verbose "[$fn] Chain Status = $($chain.ChainStatus)" $status = if ($chain.ChainStatus) { ,@($chain.ChainStatus) } else { ,@() } $subjects = if ($chain.ChainElements) { ,@($chain.ChainElements | ForEach-Object { $_.Certificate.Subject }) } else { ,@() } Write-Verbose "[$fn] Chain built. IsTrusted=$isTrusted; StatusCount=$($status.Count)" [PSCustomObject]@{ Chain = $chain IsTrusted = $isTrusted ChainStatus = $status ChainSubjects = $subjects } } finally { $sw.Stop() Write-Verbose "[$fn] Complete in $($sw.Elapsed)" } } #EndRegion '.\private\Build-CertificateChain.ps1' 45 #Region '.\private\Connect-TcpWithTimeout.ps1' -1 function Connect-TcpWithTimeout { <# .SYNOPSIS Opens a TcpClient and connects with a timeout. .OUTPUTS PSCustomObject { TcpClient, NetworkStream } #> [CmdletBinding()] param( [Parameter(Mandatory)][string]$Hostname, [Parameter(Mandatory)][int]$Port, [int]$TimeoutMs = 10000 ) $fn = $MyInvocation.MyCommand.Name $sw = [System.Diagnostics.Stopwatch]::StartNew() $TimeoutMs = [Math]::Max(1000, $TimeoutMs) Write-Verbose "[$fn] Begin (Target=$Hostname :$Port, TimeoutMs=$TimeoutMs)" $tcp = $null try { $tcp = [System.Net.Sockets.TcpClient]::new() $tcp.NoDelay = $true $task = $tcp.ConnectAsync($Hostname, $Port) if (-not $task.Wait($TimeoutMs)) { throw [System.TimeoutException]::new("Connection timeout after ${TimeoutMs}ms to $Hostname :$Port") } $netStream = $tcp.GetStream() Write-Verbose "[$fn] Connected to $Hostname :$Port" [PSCustomObject]@{ TcpClient = $tcp; NetworkStream = $netStream } } catch { try { if ($tcp) { $tcp.Dispose() } } catch {} throw } finally { $sw.Stop() Write-Verbose "[$fn] Complete in $($sw.Elapsed)" } } #EndRegion '.\private\Connect-TcpWithTimeout.ps1' 39 #Region '.\private\Format-ChainStatusStrings.ps1' -1 function Format-ChainStatusStrings { <# .SYNOPSIS Converts X509ChainStatus[] into human-readable strings. #> [CmdletBinding()] param( [Parameter(Mandatory)] [System.Security.Cryptography.X509Certificates.X509ChainStatus[]]$ChainStatus ) $fn = $MyInvocation.MyCommand.Name $sw = [System.Diagnostics.Stopwatch]::StartNew() Write-Verbose "[$fn] Begin (Count=$($ChainStatus?.Count))" try { if (-not $ChainStatus -or $ChainStatus.Count -eq 0) { return ,@() } ,@( foreach ($s in $ChainStatus) { "{0}: {1}" -f $s.Status, ($s.StatusInformation.Trim()) } ) } finally { $sw.Stop() Write-Verbose "[$fn] Complete in $($sw.Elapsed)" } } #EndRegion '.\private\Format-ChainStatusStrings.ps1' 27 #Region '.\private\Get-AIAUrls.ps1' -1 function Get-AIAUrls { <# .SYNOPSIS Extracts Authority Information Access (AIA) URLs from a certificate. #> [CmdletBinding()] param( [Parameter(Mandatory)] [System.Security.Cryptography.X509Certificates.X509Certificate2]$Cert ) $fn = $MyInvocation.MyCommand.Name $sw = [System.Diagnostics.Stopwatch]::StartNew() Write-Verbose "[$fn] Begin (Thumbprint=$($Cert.Thumbprint))" try { $oid = '1.3.6.1.5.5.7.1.1' $ext = $Cert.Extensions | Where-Object { $_.Oid.Value -eq $oid } | Select-Object -First 1 if (-not $ext) { return ,@() } try { $data = [System.Security.Cryptography.AsnEncodedData]::new($ext.Oid, $ext.RawData) $txt = $data.Format($true) $uris = ($txt -split '(,|\r?\n)') | Where-Object { $_ -match '(http|ldap)s?://' } | ForEach-Object { $_.Trim() } | Select-Object -Unique if ($uris) { return ,@($uris) } else { return ,@() } } catch { return ,@() } } finally { $sw.Stop() Write-Verbose "[$fn] Complete in $($sw.Elapsed)" } } #EndRegion '.\private\Get-AIAUrls.ps1' 36 #Region '.\private\Get-CDPUrls.ps1' -1 function Get-CDPUrls { <# .SYNOPSIS Extracts CRL Distribution Point (CDP) URLs from a certificate. #> [CmdletBinding()] param( [Parameter(Mandatory)] [System.Security.Cryptography.X509Certificates.X509Certificate2]$Cert ) $fn = $MyInvocation.MyCommand.Name $sw = [System.Diagnostics.Stopwatch]::StartNew() Write-Verbose "[$fn] Begin (Thumbprint=$($Cert.Thumbprint))" try { $oid = '2.5.29.31' $ext = $Cert.Extensions | Where-Object { $_.Oid.Value -eq $oid } | Select-Object -First 1 if (-not $ext) { return ,@() } try { $data = [System.Security.Cryptography.AsnEncodedData]::new($ext.Oid, $ext.RawData) $txt = $data.Format($true) $uris = ($txt -split '(,|\r?\n)') | Where-Object { $_ -match '(http|ldap)s?://' } | ForEach-Object { $_.Trim() } | Select-Object -Unique if ($uris) { return ,@($uris) } else { return ,@() } } catch { return ,@() } } finally { $sw.Stop() Write-Verbose "[$fn] Complete in $($sw.Elapsed)" } } #EndRegion '.\private\Get-CDPUrls.ps1' 36 #Region '.\private\Get-CertificateSAN.ps1' -1 function Get-CertificateSAN { <# .SYNOPSIS Returns DNS Subject Alternative Names from a certificate as an array (possibly empty). #> [CmdletBinding()] param( [Parameter(Mandatory)] [System.Security.Cryptography.X509Certificates.X509Certificate2]$Cert ) $fn = $MyInvocation.MyCommand.Name $sw = [System.Diagnostics.Stopwatch]::StartNew() Write-Verbose "[$fn] Begin (Thumbprint=$($Cert.Thumbprint))" try { # Prefer modern property when available $dnsProp = [System.Security.Cryptography.X509Certificates.X509Certificate2].GetProperty('DnsNameList') if ($dnsProp) { try { $val = $dnsProp.GetValue($Cert, $null) if ($val) { $names = @($val) | ForEach-Object { $_.ToString().Trim() } | Where-Object { $_ } | Select-Object -Unique return ,@($names) } } catch { } } # Fallback: parse SAN extension text $sanOid = '2.5.29.17' $ext = $Cert.Extensions | Where-Object { $_.Oid.Value -eq $sanOid } | Select-Object -First 1 if (-not $ext) { return ,@() } try { $data = New-Object System.Security.Cryptography.AsnEncodedData($ext.Oid, $ext.RawData) $txt = $data.Format($true) $names = ($txt -split '(,|\r?\n)') | ForEach-Object { $line = $_.Trim() if ($line -match 'DNS Name\=(.+)$') { $Matches[1].Trim(); return } if ($line -match 'DNS:(.+)$') { $Matches[1].Trim(); return } } | Where-Object { $_ } | Sort-Object -Unique # <- ensures deterministic order if ($names) { return ,@($names) } else { return ,@() } } catch { return ,@() } } finally { $sw.Stop() Write-Verbose "[$fn] Complete in $($sw.Elapsed)" } } #EndRegion '.\private\Get-CertificateSAN.ps1' 55 #Region '.\private\Get-HandshakeInfo.ps1' -1 function Get-HandshakeInfo { <# .SYNOPSIS Extracts negotiated TLS protocol and cipher details from an SslStream. #> [CmdletBinding()] param( [Parameter(Mandatory)] [System.Net.Security.SslStream]$SslStream ) $fn = $MyInvocation.MyCommand.Name $sw = [System.Diagnostics.Stopwatch]::StartNew() Write-Verbose "[$fn] Begin" try { $supports = Test-NegotiatedCipherSuiteSupport $negProtocol = $SslStream.SslProtocol $cipherName = $null $cipherStrength = $null $hashAlgorithm = $null $kexAlgorithm = $null $kexStrength = $null if ($supports -and $SslStream.NegotiatedCipherSuite) { try { $cipherName = $SslStream.NegotiatedCipherSuite.ToString() } catch {} try { $cipherStrength = $SslStream.CipherStrength } catch {} try { $hashAlgorithm = $SslStream.HashAlgorithm.ToString() } catch {} try { $kexAlgorithm = $SslStream.KeyExchangeAlgorithm.ToString() } catch {} try { $kexStrength = $SslStream.KeyExchangeStrength } catch {} } else { try { $cipherName = $SslStream.CipherAlgorithm.ToString() } catch {} try { $cipherStrength = $SslStream.CipherStrength } catch {} try { $hashAlgorithm = $SslStream.HashAlgorithm.ToString() } catch {} try { $kexAlgorithm = $SslStream.KeyExchangeAlgorithm.ToString() } catch {} try { $kexStrength = $SslStream.KeyExchangeStrength } catch {} } [PSCustomObject]@{ Protocol = $negProtocol.ToString() CipherSuite = $cipherName CipherStrengthBits = $cipherStrength HashAlgorithm = $hashAlgorithm KeyExchangeAlgorithm= $kexAlgorithm KeyExchangeStrength = $kexStrength } } finally { $sw.Stop() Write-Verbose "[$fn] Complete in $($sw.Elapsed)" } } #EndRegion '.\private\Get-HandshakeInfo.ps1' 52 #Region '.\private\Get-SslProtocolsEnum.ps1' -1 function Get-SslProtocolsEnum { <# .SYNOPSIS Maps string names to [System.Security.Authentication.SslProtocols] flags. .DESCRIPTION Accepts names like 'SystemDefault','Tls12','Tls13' and returns the enum. 'SystemDefault' maps to ::None to let .NET choose defaults. .PARAMETER Names Array of protocol names. .OUTPUTS System.Security.Authentication.SslProtocols #> [CmdletBinding()] param( [Parameter(Mandatory)] [string[]]$Names ) $fn = $MyInvocation.MyCommand.Name $sw = [System.Diagnostics.Stopwatch]::StartNew() Write-Verbose "[$fn] Begin (Names=$($Names -join ','))" try { $enumType = [System.Security.Authentication.SslProtocols] if ($Names -contains 'SystemDefault' -and $Names.Count -eq 1) { return [System.Security.Authentication.SslProtocols]::None } $value = [System.Security.Authentication.SslProtocols]::None foreach ($n in $Names) { if ($n -eq 'SystemDefault') { continue } $value = $value -bor [System.Enum]::Parse($enumType, $n) } if ($value -eq [System.Security.Authentication.SslProtocols]::None) { return [System.Security.Authentication.SslProtocols]::None } return $value } finally { $sw.Stop() Write-Verbose "[$fn] Complete in $($sw.Elapsed)" } } #EndRegion '.\private\Get-SslProtocolsEnum.ps1' 45 #Region '.\private\New-TLSleuthCertificateReport.ps1' -1 function New-TLSleuthCertificateReport { <# .SYNOPSIS Creates the final TLSleuth certificate report object. #> [CmdletBinding()] param( [Parameter(Mandatory)][string]$Hostname, [Parameter(Mandatory)][int]$Port, [string]$ConnectedIp, [Parameter(Mandatory)][string]$SNI, [Parameter(Mandatory)][psobject]$Handshake, [Parameter(Mandatory)][System.Security.Cryptography.X509Certificates.X509Certificate2]$Certificate, [psobject]$ChainInfo, [System.Security.Cryptography.X509Certificates.X509Chain]$CapturedChain, [string[]]$ValidationErrors = @(), [string[]]$SANs = @(), [string[]]$AIA = @(), [string[]]$CDP = @() ) $fn = $MyInvocation.MyCommand.Name $sw = [System.Diagnostics.Stopwatch]::StartNew() Write-Verbose "[$fn] Begin (Host=$Hostname :$Port, Protocol=$($Handshake.Protocol))" try { $now = [DateTimeOffset]::UtcNow $pubKeyAlgo = $Certificate.PublicKey.Oid.FriendlyName $keySize = $Certificate.PublicKey.Key.KeySize $sigAlgo = $Certificate.SignatureAlgorithm.FriendlyName $isTrusted = $null $chainSubjects = ,@() $chainStatus = ,@() if ($ChainInfo) { $isTrusted = $ChainInfo.IsTrusted $chainSubjects = $ChainInfo.ChainSubjects $chainStatus = $ChainInfo.ChainStatus } elseif ($CapturedChain) { $isTrusted = ($CapturedChain.ChainStatus.Count -eq 0) $chainSubjects = if ($CapturedChain.ChainElements) { ,@($CapturedChain.ChainElements | ForEach-Object { $_.Certificate.Subject }) } else { ,@() } $chainStatus = if ($CapturedChain.ChainStatus) { ,@($CapturedChain.ChainStatus) } else { ,@() } } $chainStatusStrings = if ($chainStatus) { Format-ChainStatusStrings -ChainStatus $chainStatus } else { ,@() } $keyExchange = if ($Handshake.KeyExchangeStrength) { "{0} ({1}-bit)" -f $Handshake.KeyExchangeAlgorithm, $Handshake.KeyExchangeStrength } else { $Handshake.KeyExchangeAlgorithm } Write-Verbose "[$fn] Hello from Report" [PSCustomObject]@{ PSTypeName = 'TLSleuth.CertificateReport' Host = $Hostname Port = $Port ConnectedIp = $ConnectedIp SNI = $SNI Protocol = $Handshake.Protocol CipherSuite = $Handshake.CipherSuite CipherStrengthBits = $Handshake.CipherStrengthBits HashAlgorithm = $Handshake.HashAlgorithm KeyExchange = $keyExchange Certificate = [PSCustomObject]@{ Subject = $Certificate.Subject CommonName = ($Certificate.GetNameInfo([System.Security.Cryptography.X509Certificates.X509NameType]::SimpleName, $false)) Issuer = $Certificate.Issuer SerialNumber = $Certificate.SerialNumber Thumbprint = $Certificate.Thumbprint NotBefore = $Certificate.NotBefore NotAfter = $Certificate.NotAfter DaysUntilExpiry = [int]([Math]::Floor(($Certificate.NotAfter.ToUniversalTime() - $now.UtcDateTime).TotalDays)) SignatureAlgorithm = $sigAlgo PublicKeyAlgorithm = $pubKeyAlgo KeySize = $keySize SANs = @($SANs) AIA = @($AIA) CRLDistribution = @($CDP) IsSelfSigned = Test-IsSelfSigned -Cert $Certificate } IsTrusted = $isTrusted ChainSubjects = $chainSubjects ChainStatus = $chainStatusStrings ValidationErrors = @($ValidationErrors | ForEach-Object { $_ }) RawCertificate = $Certificate } } catch { Write-Verbose "[$fn] Report failed: $($_.Exception.Message)" } finally { $sw.Stop() Write-Verbose "[$fn] Complete in $($sw.Elapsed)" } } #EndRegion '.\private\New-TLSleuthCertificateReport.ps1' 94 #Region '.\private\Resolve-Endpoint.ps1' -1 function Resolve-Endpoint { <# .SYNOPSIS Best-effort DNS resolution of a host to an IP address. #> [CmdletBinding()] param( [Parameter(Mandatory)] [string]$Hostname ) $fn = $MyInvocation.MyCommand.Name $sw = [System.Diagnostics.Stopwatch]::StartNew() Write-Verbose "[$fn] Begin (Hostname=$Hostname)" try { try { $ips = [System.Net.Dns]::GetHostAddresses($Hostname) $ip = if ($ips -and $ips.Length -gt 0) { $ips[0] } else { $null } if ($ip) { Write-Verbose "[$fn] Resolved $Hostname -> $ip" } return $ip } catch { Write-Verbose "[$fn] Resolution failed: $($_.Exception.Message)" return $null } } finally { $sw.Stop() Write-Verbose "[$fn] Complete in $($sw.Elapsed)" } } #EndRegion '.\private\Resolve-Endpoint.ps1' 30 #Region '.\private\Start-TlsHandshake.ps1' -1 function Start-TlsHandshake { <# .SYNOPSIS Performs an SNI-aware TLS handshake over an existing NetworkStream. .OUTPUTS PSCustomObject { SslStream, RemoteCertificate, CapturedChain, ValidationErrors } #> [CmdletBinding()] param( [Parameter(Mandatory)][System.IO.Stream]$NetworkStream, [Parameter(Mandatory)][string]$TargetHost, [Parameter(Mandatory)][System.Security.Authentication.SslProtocols]$Protocols, [switch]$CheckRevocation, [int]$TimeoutMs = 10000 ) $fn = $MyInvocation.MyCommand.Name $sw = [System.Diagnostics.Stopwatch]::StartNew() $TimeoutMs = [Math]::Max(1000, $TimeoutMs) Write-Verbose "[$fn] Begin (SNI=$TargetHost, Protocols=$Protocols, TimeoutMs=$TimeoutMs, Revocation=$([bool]$CheckRevocation))" $sslStream = $null try { $certErrors = New-Object System.Collections.Generic.List[string] $capturedChain = $null $validationCallback = { param( [object]$sender, [System.Security.Cryptography.X509Certificates.X509Certificate]$certificate, [System.Security.Cryptography.X509Certificates.X509Chain]$chain, [System.Net.Security.SslPolicyErrors]$sslPolicyErrors ) try { if ($chain -and $certificate) { $tmp = [System.Security.Cryptography.X509Certificates.X509Chain]::new() $tmp.ChainPolicy.RevocationMode = $chain.ChainPolicy.RevocationMode $tmp.ChainPolicy.RevocationFlag = $chain.ChainPolicy.RevocationFlag $tmp.ChainPolicy.VerificationFlags= $chain.ChainPolicy.VerificationFlags $tmp.ChainPolicy.VerificationTime = $chain.ChainPolicy.VerificationTime [void]$tmp.Build([System.Security.Cryptography.X509Certificates.X509Certificate2]$certificate) $script:capturedChain = $tmp } } catch {} if ($sslPolicyErrors -ne [System.Net.Security.SslPolicyErrors]::None) { [void]$certErrors.Add($sslPolicyErrors.ToString()) } return $true } $sslStream = [System.Net.Security.SslStream]::new($NetworkStream, $false, $validationCallback) try { $opts = [System.Net.Security.SslClientAuthenticationOptions]::new() $opts.TargetHost = $TargetHost $opts.EnabledSslProtocols = $Protocols $opts.CertificateRevocationCheckMode = if ($CheckRevocation) { [System.Security.Cryptography.X509Certificates.X509RevocationMode]::Online } else { [System.Security.Cryptography.X509Certificates.X509RevocationMode]::NoCheck } $cts = [System.Threading.CancellationTokenSource]::new() try { $task = $sslStream.AuthenticateAsClientAsync($opts, $cts.Token) if (-not $task.Wait($TimeoutMs)) { throw [System.TimeoutException]::new("TLS handshake timeout after ${TimeoutMs}ms") } } finally { try { $cts.Dispose() } catch {} } } catch { # Legacy path $sslStream.ReadTimeout = $TimeoutMs $sslStream.WriteTimeout = $TimeoutMs $clientCerts = [System.Security.Cryptography.X509Certificates.X509CertificateCollection]::new() $rev = [bool]$CheckRevocation $sslStream.AuthenticateAsClient($TargetHost, $clientCerts, $Protocols, $rev) } $remoteCert = $sslStream.RemoteCertificate if (-not $remoteCert) { throw "No certificate was presented by the server." } if ($remoteCert -isnot [System.Security.Cryptography.X509Certificates.X509Certificate2]) { $remoteCert = [System.Security.Cryptography.X509Certificates.X509Certificate2]::new($remoteCert) } Write-Verbose "[$fn] Handshake complete. Protocol=$($sslStream.SslProtocol); CertCN=$($remoteCert.GetNameInfo([System.Security.Cryptography.X509Certificates.X509NameType]::DnsName, $false))" [PSCustomObject]@{ SslStream = $sslStream RemoteCertificate = $remoteCert CapturedChain = $capturedChain ValidationErrors = ,@($certErrors) } } catch { try { if ($sslStream) { $sslStream.Dispose() } } catch {} throw } finally { $sw.Stop() Write-Verbose "[$fn] Complete in $($sw.Elapsed)" } } #EndRegion '.\private\Start-TlsHandshake.ps1' 99 #Region '.\private\Test-IsSelfSigned.ps1' -1 function Test-IsSelfSigned { <# .SYNOPSIS Returns $true if the certificate is self-signed (subject == issuer). #> [CmdletBinding()] param( [Parameter(Mandatory)] [System.Security.Cryptography.X509Certificates.X509Certificate2]$Cert ) $fn = $MyInvocation.MyCommand.Name $sw = [System.Diagnostics.Stopwatch]::StartNew() Write-Verbose "[$fn] Begin (Thumbprint=$($Cert.Thumbprint))" try { return $Cert.Subject -eq $Cert.Issuer } finally { $sw.Stop() Write-Verbose "[$fn] Complete in $($sw.Elapsed)" } } #EndRegion '.\private\Test-IsSelfSigned.ps1' 21 #Region '.\private\Test-NegotiatedCipherSuiteSupport.ps1' -1 function Test-NegotiatedCipherSuiteSupport { <# .SYNOPSIS Detects whether .NET exposes SslStream.NegotiatedCipherSuite property. #> [CmdletBinding()] param() $fn = $MyInvocation.MyCommand.Name $sw = [System.Diagnostics.Stopwatch]::StartNew() Write-Verbose "[$fn] Begin" try { try { $prop = [System.Net.Security.SslStream].GetProperty('NegotiatedCipherSuite') return [bool]$prop } catch { return $false } } finally { $sw.Stop() Write-Verbose "[$fn] Complete in $($sw.Elapsed)" } } #EndRegion '.\private\Test-NegotiatedCipherSuiteSupport.ps1' 24 #Region '.\public\Get-TLSCertificate.ps1' -1 function Get-TLSCertificate { <# .SYNOPSIS Retrieves the SSL/TLS certificate from a remote server and reports handshake details. .DESCRIPTION Orchestrates smaller private helpers to connect, handshake, collect details, optionally build a validation chain, and assemble a TLSleuth report. .PARAMETER Hostname DNS name or IP of the server. .PARAMETER Port TCP port to connect to. Defaults to 443. .PARAMETER ServerName SNI server name (TargetHost). Defaults to -Host. .PARAMETER TlsProtocols TLS protocol(s) to allow. Defaults to SystemDefault. .PARAMETER TimeoutSec Connection + handshake timeout in seconds. Default: 10. .PARAMETER IncludeChain If specified, builds chain/trust info locally. .PARAMETER CheckRevocation If specified, attempts revocation checking (OCSP/CRL). .PARAMETER RawCertificate If specified, outputs the raw X509Certificate2. .OUTPUTS TLSleuth.CertificateReport or X509Certificate2 #> [CmdletBinding()] [OutputType([pscustomobject], [System.Security.Cryptography.X509Certificates.X509Certificate2])] param( [Parameter(Mandatory, ValueFromPipeline, ValueFromPipelineByPropertyName)] [Alias('DnsName','ComputerName','Target','Name','CN')] [string]$Hostname, [int]$Port = 443, [string]$ServerName, [ValidateSet('SystemDefault','Ssl3','Tls','Tls11','Tls12','Tls13')] [string[]]$TlsProtocols = @('SystemDefault'), [int]$TimeoutSec = 10 ) begin { $fn = $MyInvocation.MyCommand.Name $pipelineSw = [System.Diagnostics.Stopwatch]::StartNew() Write-Verbose "[$fn] Begin (TimeoutSec=$TimeoutSec, Protocols=$($TlsProtocols -join ','))" $sslProtocolsEnum = Get-SslProtocolsEnum -Names $TlsProtocols $processed = 0 } process { $processed++ Write-Verbose "[$fn] Processing Host=$Hostname" $targetHost = if ($ServerName) { $ServerName } else { $Hostname } if ([string]::IsNullOrWhiteSpace($targetHost)) { throw "ServerName/Host is empty." } $resolvedIp = Resolve-Endpoint -Host $Hostname Write-Verbose "[$fn] Resolved $Hostname=$resolvedIp" $timeoutMs = [Math]::Max(1000, $TimeoutSec * 1000) $tcp = $null; $net = $null; $sslInfo = $null try { $conn = Connect-TcpWithTimeout -Host $resolvedIp -Port $Port -TimeoutMs $timeoutMs $tcp = $conn.TcpClient $net = $conn.NetworkStream $sslInfo = Start-TlsHandshake -NetworkStream $net -TargetHost $targetHost -Protocols $sslProtocolsEnum -CheckRevocation:$CheckRevocation -TimeoutMs $timeoutMs $remoteCert = $sslInfo.RemoteCertificate return $remoteCert } catch { $err = $_ $msg = if ($err.Exception) { $err.Exception.Message } else { $err.ToString() } $ex = [System.Exception]::new("Get-TLSleuthCertificate failed for $Hostname :$Port - $msg", $err.Exception) $record = New-Object System.Management.Automation.ErrorRecord( $ex, 'TLSleuth.GetTLSleuthCertificateFailed', [System.Management.Automation.ErrorCategory]::InvalidOperation, $Hostname ) Write-Error $record } finally { try { if ($sslInfo -and $sslInfo.SslStream) { $sslInfo.SslStream.Dispose() } } catch {} try { if ($net) { $net.Dispose() } } catch {} try { if ($tcp) { $tcp.Close(); $tcp.Dispose() } } catch {} } } end { $pipelineSw.Stop() Write-Verbose "[$($MyInvocation.MyCommand.Name)] Complete (Processed=$processed) in $($pipelineSw.Elapsed)" } } #EndRegion '.\public\Get-TLSCertificate.ps1' 106 #Region '.\public\Get-TLSleuthCertificate.ps1' -1 function Get-TLSleuthCertificate { <# .SYNOPSIS Retrieves the SSL/TLS certificate from a remote server and reports handshake details. .DESCRIPTION Orchestrates smaller private helpers to connect, handshake, collect details, optionally build a validation chain, and assemble a TLSleuth report. .PARAMETER Hostname DNS name or IP of the server. .PARAMETER Port TCP port to connect to. Defaults to 443. .PARAMETER ServerName SNI server name (TargetHost). Defaults to -Host. .PARAMETER TlsProtocols TLS protocol(s) to allow. Defaults to SystemDefault. .PARAMETER TimeoutSec Connection + handshake timeout in seconds. Default: 10. .PARAMETER IncludeChain If specified, builds chain/trust info locally. .PARAMETER CheckRevocation If specified, attempts revocation checking (OCSP/CRL). .PARAMETER RawCertificate If specified, outputs the raw X509Certificate2. .OUTPUTS TLSleuth.CertificateReport or X509Certificate2 #> [CmdletBinding()] [OutputType([pscustomobject], [System.Security.Cryptography.X509Certificates.X509Certificate2])] param( [Parameter(Mandatory, ValueFromPipeline, ValueFromPipelineByPropertyName)] [Alias('DnsName','ComputerName','Target','Name','CN')] [string]$Hostname, [int]$Port = 443, [string]$ServerName, [ValidateSet('SystemDefault','Ssl3','Tls','Tls11','Tls12','Tls13')] [string[]]$TlsProtocols = @('SystemDefault'), [int]$TimeoutSec = 10, [switch]$IncludeChain, [switch]$CheckRevocation, [switch]$RawCertificate ) begin { $fn = $MyInvocation.MyCommand.Name $pipelineSw = [System.Diagnostics.Stopwatch]::StartNew() Write-Verbose "[$fn] Begin (TimeoutSec=$TimeoutSec, Protocols=$($TlsProtocols -join ','))" $sslProtocolsEnum = Get-SslProtocolsEnum -Names $TlsProtocols $processed = 0 } process { $processed++ Write-Verbose "[$fn] Processing Host=$Hostname" $targetHost = if ($ServerName) { $ServerName } else { $Hostname } if ([string]::IsNullOrWhiteSpace($targetHost)) { throw "ServerName/Host is empty." } $resolvedIp = Resolve-Endpoint -Host $Hostname Write-Verbose "[$fn] Resolved $Hostname=$resolvedIp" $timeoutMs = [Math]::Max(1000, $TimeoutSec * 1000) $tcp = $null; $net = $null; $sslInfo = $null try { $conn = Connect-TcpWithTimeout -Host $resolvedIp -Port $Port -TimeoutMs $timeoutMs $tcp = $conn.TcpClient $net = $conn.NetworkStream $sslInfo = Start-TlsHandshake -NetworkStream $net -TargetHost $targetHost -Protocols $sslProtocolsEnum -CheckRevocation:$CheckRevocation -TimeoutMs $timeoutMs $remoteCert = $sslInfo.RemoteCertificate if ($RawCertificate) { $remoteCert; return } $hs = Get-HandshakeInfo -SslStream $sslInfo.SslStream $aia = Get-AIAUrls -Cert $remoteCert $cdp = Get-CDPUrls -Cert $remoteCert $sans = Get-CertificateSAN -Cert $remoteCert $chainInfo = $null if ($IncludeChain) { $chainInfo = Build-CertificateChain -Certificate $remoteCert -CheckRevocation:$CheckRevocation } $report = New-TLSleuthCertificateReport ` -Host $Hostname -Port $Port -ConnectedIp $resolvedIp.ToString() -SNI $targetHost ` -Handshake $hs -Certificate $remoteCert -ChainInfo $chainInfo ` -CapturedChain $sslInfo.CapturedChain -ValidationErrors $sslInfo.ValidationErrors ` -SANs $sans -AIA $aia -CDP $cdp $report } catch { $err = $_ $msg = if ($err.Exception) { $err.Exception.Message } else { $err.ToString() } $ex = [System.Exception]::new("Get-TLSleuthCertificate failed for $Hostname :$Port - $msg", $err.Exception) $record = New-Object System.Management.Automation.ErrorRecord( $ex, 'TLSleuth.GetTLSleuthCertificateFailed', [System.Management.Automation.ErrorCategory]::InvalidOperation, $Hostname ) Write-Error $record } finally { try { if ($sslInfo -and $sslInfo.SslStream) { $sslInfo.SslStream.Dispose() } } catch {} try { if ($net) { $net.Dispose() } } catch {} try { if ($tcp) { $tcp.Close(); $tcp.Dispose() } } catch {} } } end { $pipelineSw.Stop() Write-Verbose "[$($MyInvocation.MyCommand.Name)] Complete (Processed=$processed) in $($pipelineSw.Elapsed)" } } #EndRegion '.\public\Get-TLSleuthCertificate.ps1' 126 |