Test-TlsProtocols.psm1
<# .DESCRIPTION Outputs the SSL/TLS protocols that the client is able to successfully use to connect to a server using fqdn or ip. Optionally outputs remote certificate information. Optionally exports remote certificates in .cer format. .NOTES Special thanks to Chris Duck's hard work from 2014 that inspired me to get started on this project. You can learn more about it on his blog at http://blog.whatsupduck.net/2014/10/checking-ssl-and-tls-versions-with-powershell.html Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. .LINK https://github.com/TechnologyAnimal/Test-TlsProtocols .PARAMETER Server The fully qualified domain name or IP address of the remote computer to connect to. * Dns will resolve an ip address to a fully qualified domain name. * Using an IP address will technically work, though, a DNS lookup to resolve the server FDQN will be used. If an IP address hosts multiple servers, an unpredictable FQDN will get selected as the FQDN to test. The end result may not get routed to the correct server. .PARAMETER Port A list of remote ports to connect to. The default is 443. Each additional port will return another result. .PARAMETER ProtocolName A list of protocols to test. Requires that the client system supports each protocol to test. Some common examples: Tls13, Tls12, Tls11, Tls, Ssl3, Ssl2 .PARAMETER IncludeErrorMessages This switch will include detailed error messages about failed connections for each tls protocol. .PARAMETER IncludeRemoteCertificateInfo This switch will return CertificateThumbprint, CertificateSubject, CertificateIssuer, CertificateIssued, CertificateExpires and SignatureAlgorithm in addition to tls protocol information. .PARAMETER ReturnRemoteCertificateOnly Enabling this switch will only return the remote system's certificate as a System.Security.Cryptography.X509Certificates.X509Certificate2 object. .PARAMETER ExportRemoteCertificate Enabling this switch will export the remote system's certificate as a $fqdn.cer file in the path of this script. .PARAMETER TimeoutSeconds This will set the amount of seconds to wait on Test-Connection results before determining the system is unreachable. If a remote system port is unreachable, the script will not attempt to establish a socket connection and all supported protocols will be unknown. Default value is 2 seconds. .PARAMETER OutputFormat This will convert the results to the corresponding output object, and if appropriate, and format. See below for a description of what each option returns as. The default is PSObject. Csv returns a System.String object in CSV format. Json returns a System.String object in JSON format. OrderedDictionary returns a System.Collections.Specialized.OrderedDictionary object. PSObject returns a System.Management.Automation.PSCustomObject object. Xml returns a System.Xml.XmlDocument object. .PARAMETER WhatIf This will list the Server, Ports, and ProtocolNames that would get tested if this switch is omitted or set to false. .EXAMPLE Test-TlsProtocols -Server "github.com" -IncludeRemoteCertificateInfo Fqdn : github.com IP : 192.30.253.113 Port : 443 CertificateThumbprint : CA06F56B258B7A0D4F2B05470939478651151984 CertificateSubject : CN=github.com, O="GitHub, Inc.", L=San Francisco, S=California, C=US, SERIALNUMBER=5157550, OID.1.3.6.1.4.1.311.60.2.1.2=Delaware, OID.1.3.6.1.4.1.311.60.2.1.3=US, OID.2.5.4.15=Private Organization CertificateIssuer : CN=DigiCert SHA2 Extended Validation Server CA, OU=www.digicert.com, O=DigiCert Inc, C=US CertificateIssued : 5/7/2018 5:00:00 PM CertificateExpires : 6/3/2020 5:00:00 AM SignatureAlgorithm : sha256RSA Ssl2 : False Ssl3 : False Tls : False Tls11 : False Tls12 : True Tls13 : True .EXAMPLE Test-TlsProtocols -Server "github.com" -OutputFormat PSObject Fqdn : github.com IP : 140.82.114.3 Port : 443 Ssl2 : False Ssl3 : False Tls : False Tls11 : False Tls12 : True Tls13 : True .EXAMPLE Test-TlsProtocols -Server "google.com" -OutputFormat Json { "Fqdn": "google.com", "IP": "216.239.34.117", "Port": 443, "Ssl2": false, "Ssl3": false, "Tls": true, "Tls11": true, "Tls12": true, "Tls13": true } .Example Test-TlsProtocols -Server "google.com" -ReturnRemoteCertificateOnly Thumbprint Subject EnhancedKeyUsageList ---------- ------- -------------------- 7D0384E3195E04043DBED29FF58815857278240C CN=*.google.com, O=… #> function Test-TlsProtocols { [cmdletbinding(SupportsShouldProcess)] param( [Parameter(Mandatory)][string]$Server, [ValidateRange(1,65535)] [int32[]]$Port = 443, [string[]]$ProtocolName, [ValidateSet("PSObject", "Csv", "Json", "OrderedDictionary", "Xml")] [String]$OutputFormat = "PSObject", [switch]$ExportRemoteCertificate, [switch]$IncludeErrorMessages, [switch]$IncludeRemoteCertificateInfo, [switch]$ReturnRemoteCertificateOnly, [ValidateSet(1, 2, 3, 4, 5)][int32]$TimeoutSeconds = 2 ) begin { # Validate input # TO-DO: Add client TLS configuration settings validation, i.e. check registry for supported client tls protocols and the *nix equivalent. # Check all Ssl/Tls protocols $SupportedProtocolNames = ([System.Security.Authentication.SslProtocols]).GetEnumValues().Where{ $_ -ne 'Default' -and $_ -ne 'None' } Write-Verbose "Supported tls protocols:" $SupportedProtocolNames.ForEach{ Write-Verbose $_ } if (-not $ProtocolName){ Write-Verbose "No tls protocols specified. Defaulting to test all support tls protocols." $ProtocolName = $SupportedProtocolNames } elseif ($UnsupportedProtocolNames = $ProtocolName.Where{ $_ -notin $SupportedProtocolNames }) { Write-Verbose "Unsupported tls protocol(s) specified. Unable to complete request. " Write-Error -ErrorAction Stop ( "Unknown protocol name(s). Please use names from the list of protocol names supported on this system ({0}). You used: {1}" -f ($SupportedProtocolNames -join ", "), ($UnsupportedProtocolNames -join ", ") ) } # Resolve input if ($Server -as [IPAddress]) { try { $Fqdn = [System.Net.DNS]::GetHostByAddress($Server).HostName $Ip = $Server Write-Verbose "Server is an IP address with FQDN: $Fqdn" } # TO-DO: Should skip process block, but the code gets messy when accounting for all switches to keep objects the same. # This is important when results are exported to a csv file. catch { Write-Verbose "Unable to resolve IP address $Server to fqdn." } } else { $Fqdn = $Server $Ip = [System.Net.DNS]::GetHostByName($Server).AddressList.IPAddressToString -join ", " Write-Verbose "Server is an FQDN with the following IP addresses: $ip" } } process { # TO-DO: Add option to enable RemoteCertificateValidationCallback (current implementation accepts all certificates) Write-Verbose "Scanning $($port.count) ports:" $Port.ForEach{ Write-Verbose $_ } $Port.ForEach{ $p = $_ $ProtocolStatus = [Ordered]@{ Fqdn = $Fqdn IP = $Ip Port = $p } [PSCustomObject]$ProtocolStatus.ForEach{ Write-Verbose $_ } if ($pscmdlet.ShouldProcess($Server, "Test the following protocols: $Name")) { if ($PSVersionTable.PSVersion.Major -ge 6) { $OpenPort = Test-Connection $Server -TCPPort $p -TimeoutSeconds $TimeoutSeconds } else { $OpenPort = (Test-NetConnection $Server -Port $p).TcpTestSucceeded } Write-Verbose "Connection to $Server`:$p is available - $OpenPort" if ($OpenPort) { # Retrieve remote certificate information when IncludeRemoteCertificateInfo switch is enabled. if ($IncludeRemoteCertificateInfo) { Write-Verbose "Including remote certificate information." $ProtocolStatus += [ordered]@{ CertificateThumbprint = 'unknown' CertificateSubject = 'unknown' CertificateIssuer = 'unknown' CertificateIssued = 'unknown' CertificateExpires = 'unknown' SignatureAlgorithm = 'unknown' } } $ProtocolName.ForEach{ $Name = $_ Write-Verbose "Starting test on $Name" $ProtocolStatus.Add($Name, 'unknown') if ($IncludeErrorMessages) { $ProtocolStatus.Add("$Name`ErrorMsg", $false) } try { $Socket = [System.Net.Sockets.Socket]::new([System.Net.Sockets.SocketType]::Stream, [System.Net.Sockets.ProtocolType]::Tcp) Write-Verbose "Attempting socket connection to $fqdn`:$p" $Socket.Connect($fqdn, $p) Write-Verbose "Connection succeeded." $NetStream = [System.Net.Sockets.NetworkStream]::new($Socket, $true) $SslStream = [System.Net.Security.SslStream]::new($NetStream, $true, { $true }) # Ignore certificate validation errors Write-Verbose "Attempting to authenticate to $fqdn as a client over $Name" $SslStream.AuthenticateAsClient($fqdn, $null, $Name, $false) $ProtocolStatus[$Name] = $true # success Write-Verbose "Successfully authenticated to $fqdn`:$p" $RemoteCertificate = [System.Security.Cryptography.X509Certificates.X509Certificate2]$SslStream.RemoteCertificate if ($IncludeRemoteCertificateInfo) { # Store remote certificate information if it hasn't already been collected if ($ProtocolStatus.CertificateThumbprint -eq 'unknown' -and $RemoteCertificate.Thumbprint) { $ProtocolStatus["CertificateThumbprint"] = $RemoteCertificate.Thumbprint $ProtocolStatus["CertificateSubject"] = $RemoteCertificate.Subject $ProtocolStatus["CertificateIssuer"] = $RemoteCertificate.Issuer $ProtocolStatus["CertificateIssued"] = $RemoteCertificate.NotBefore $ProtocolStatus["CertificateExpires"] = $RemoteCertificate.NotAfter $ProtocolStatus["SignatureAlgorithm"] = $RemoteCertificate.SignatureAlgorithm.FriendlyName } } if ($ExportRemoteCertificate) { $CertPath = "$fqdn.cer" if (-not (Test-Path $CertPath)) { Write-Host "Exporting $fqdn.cer to $($(Get-Location).path)" -ForegroundColor Green $RemoteCertificate.Export('Cert') | Set-Content "$fqdn.cer" -AsByteStream } } if ($ReturnRemoteCertificateOnly) { Write-Verbose "Returning $fqdn remote certificate only." $RemoteCertificate break; } } catch { $ProtocolStatus[$Name] = $false # failed to establish tls connection Write-Verbose "Unable to establish tls connection with $fqdn`:$p over $Name" # Collect detailed error message about why the tls connection failed if ($IncludeErrorMessages) { $e = $error[0] $NestedException = $e.Exception.InnerException.InnerException.Message if ($NestedException) { $emsg = $NestedException } else { $emsg = $e.Exception.InnerException.Message } Write-Verbose $emsg $ProtocolStatus["$Name`ErrorMsg"] = $emsg } } finally { # Free up system memory/garbage collection Write-Verbose "Garbage collection." if ($SslStream) { $SslStream.Dispose() } if ($NetStream) { $NetStream.Dispose() } if ($Socket) { $Socket.Dispose() } } } } else { # Supported Tls protocols are unknown when a connection cannot be established. Write-Verbose "Supported Tls protocols are unknown when a connection cannot be established." $ProtocolName.ForEach{ $Name = $_ $ProtocolStatus.Add($Name, 'unknown') if ($IncludeErrorMessages) { $ProtocolStatus.Add("$Name`ErrorMsg", "Could not connect to $server on TCP port $p`.") } } } Export-ProtocolStatus -ProtocolStatus $ProtocolStatus -OutputFormat $OutputFormat } } } } # Test-TlsProtocols function Export-ProtocolStatus { [CmdletBinding()] param ( $ProtocolStatus, $OutputFormat ) process { if ([string]::IsNullOrWhiteSpace($OutputFormat)) { [PSCustomObject]$ProtocolStatus } else { # Various switches to generate output in desired format of choice switch ($OutputFormat) { "Csv" { [PSCustomObject]$ProtocolStatus | ConvertTo-Csv -NoTypeInformation } "Json" { [PSCustomObject]$ProtocolStatus | ConvertTo-Json } "OrderedDictionary" { $ProtocolStatus } # Ordered HashTable "PSObject" { [PSCustomObject]$ProtocolStatus } "Xml" { [PSCustomObject]$ProtocolStatus | ConvertTo-Xml -NoTypeInformation } } } } } Export-ModuleMember -Function Test-TlsProtocols |