Test-TlsProtocols.psm1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
<#
.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