-
-
Save jborean93/b494d64dcfba5e8187fbcc77b576599a to your computer and use it in GitHub Desktop.
| # Copyright: (c) 2022, Jordan Borean (@jborean93) <[email protected]> | |
| # MIT License (see LICENSE or https://opensource.org/licenses/MIT) | |
| Function Trace-TlsHandshake { | |
| <# | |
| .SYNOPSIS | |
| TLS Handshake Diagnostics. | |
| .DESCRIPTION | |
| Performs a TLS handshake and returns diagnostic information about that | |
| exchange. It can also be used to capture the raw TLS packets used in the | |
| exchange through the -CapturePacket switch. | |
| .PARAMETER HostName | |
| The hostname to connect to. | |
| .PARAMETER Port | |
| The port to connect to, defaults to 443 which is commonly used for https. | |
| .PARAMETER Protocol | |
| The TLS protocol to restrict the handshake to. The default will use the | |
| OS defaults but this can be used to use a specific protocol. | |
| .PARAMETER CipherSuite | |
| Override the cipher suite selection policy. The default will use the OS | |
| defaults. Currently only Linux and macOS can define an explicit cipher | |
| suite policy. The dotnet API does not support this for Windows | |
| https://github.com/dotnet/runtime/issues/23818. | |
| .PARAMETER CapturePacket | |
| Capture the raw TLS packets and store them in the output Packets property. | |
| This is experimental but can be used to analyze data not available through | |
| PowerShell/.NET. | |
| .EXAMPLE | |
| Trace-TlsHandshake -HostName google.com | |
| .OUTPUTS | |
| This cmdlet outputs an object with the following properties: | |
| Protocol | |
| The TLS protocol that was negotiated. The values correspond to the | |
| SslProtocols enum | |
| https://docs.microsoft.com/en-us/dotnet/api/system.security.authentication.sslprotocols?view=net-6.0 | |
| Cipher | |
| The cipher suite which was negotiated. The values correspond to the | |
| TlsCipherSuite enum | |
| https://docs.microsoft.com/en-us/dotnet/api/system.net.security.tlsciphersuite?view=net-6.0 | |
| KeyStrength | |
| The strength of the key exchange algorithm that was negotiated. | |
| Certificate | |
| The certificate of the target host. | |
| Chain | |
| A chain of certificate authorities associated with the remote | |
| certificate. | |
| VerificationStatus | |
| The cert verifcation result. The values can be set to the following: | |
| None - No errors, verification succeeded | |
| RemoteCertificateChainErrors - Errors in the chain | |
| RemoteCertificateNameMismatch - Certificate name mismatched | |
| RemoteCertificateNotAvailable - No certificate was available | |
| The enums are based off | |
| https://docs.microsoft.com/en-us/dotnet/api/system.net.security.sslpolicyerrors?view=net-6.0 | |
| Packets: | |
| The raw TLS packets that were exchanged from the client and host. This | |
| will be empty if -CapturePacket was not set. | |
| The object in the Packets list contain the following properties: | |
| Direction: | |
| The direction of the packet; Outgoing for packets sent from the client | |
| and Incoming for packets sent from the server. | |
| ContentType: | |
| The raw content type of the TLS record. | |
| Version: | |
| The raw version id of the TLS record. | |
| Length: | |
| The length of the TLS record payload | |
| Data: | |
| The full TLS record packet, this contains both the TLS record header | |
| (first 5 bytes) as well as the remaining payload (size of Length). | |
| .NOTES | |
| This cmdlet is designed to replicate some of the same functionality that | |
| exists in the openssl s_client -connect command. | |
| This must run with PowerShell 7+. | |
| #> | |
| [CmdletBinding()] | |
| param ( | |
| [Parameter(Mandatory)] | |
| [string] | |
| $HostName, | |
| [Parameter()] | |
| [int] | |
| $Port = 443, | |
| [Parameter()] | |
| [System.Security.Authentication.SslProtocols] | |
| $Protocol = [System.Security.Authentication.SslProtocols]::None, | |
| [Parameter()] | |
| [System.Net.Security.CipherSuitesPolicy] | |
| $CipherSuite = $null, | |
| [Parameter()] | |
| [switch] | |
| $CapturePacket | |
| ) | |
| $bufferSize = 16KB | |
| $pipeName = "tls-test-$([Guid]::NewGuid())" | |
| try { | |
| Write-Verbose -Message "Connecting to $($HostName):$Port" | |
| $socket = [System.Net.Sockets.TcpClient]::new($HostName, $Port) | |
| $socketStream = $socket.GetStream() | |
| Write-Verbose -Message "Creating named pipe for packet capture called '$pipeName'" | |
| $serverPipe = [System.IO.Pipes.NamedPipeServerStream]::new( | |
| $pipeName, | |
| [System.IO.Pipes.PipeDirection]::InOut, | |
| 1, | |
| [System.IO.Pipes.PipeTransmissionMode]::Byte, | |
| [System.IO.Pipes.PipeOptions]"Asynchronous, CurrentUserOnly", | |
| $bufferSize, | |
| $bufferSize | |
| ) | |
| $clientPipe = [System.IO.Pipes.NamedPipeClientStream]::new( | |
| ".", | |
| $pipeName, | |
| [System.IO.Pipes.PipeDirection]::InOut, | |
| [System.IO.Pipes.PipeOptions]"Asynchronous, CurrentUserOnly" | |
| ) | |
| Write-Verbose -Message "Connecting client and server named pipes" | |
| $waitTask = $serverPipe.WaitForConnectionAsync() | |
| $clientPipe.Connect() | |
| while (-not $waitTask.AsyncWaitHandle.WaitOne(200)) { } | |
| $null = $waitTask.GetAwaiter().GetResult() | |
| $tlsPipeRecorder = { | |
| [CmdletBinding()] | |
| param ( | |
| [Parameter(Mandatory)] | |
| [string] | |
| $Id, | |
| [Parameter(Mandatory)] | |
| [System.IO.Stream] | |
| $Incoming, | |
| [Parameter(Mandatory)] | |
| [System.IO.Stream] | |
| $Outgoing, | |
| [Parameter(Mandatory)] | |
| [AllowEmptyCollection()] | |
| [System.Collections.Generic.List[PSObject]] | |
| $MessageList, | |
| [Parameter(Mandatory)] | |
| [System.Threading.CancellationToken] | |
| $CancelToken | |
| ) | |
| try { | |
| $buffer = [byte[]]::new(16KB) | |
| while ($true) { | |
| $task = $Incoming.ReadAsync($buffer, 0, 5, $CancelToken) | |
| while (-not $task.AsyncWaitHandle.WaitOne(200)) { } | |
| $null = $task.GetAwaiter().GetResult() | |
| # The length of the TLS Record is a big endian UInt16 value | |
| $msgLength = (([uint16]$buffer[3]) -shl 8) -bor $buffer[4] | |
| $null = $Incoming.Read($buffer, 5, $msgLength) | |
| $Outgoing.Write($buffer, 0, $msgLength + 5) | |
| $Outgoing.Flush() | |
| $msgObj = [PSCustomObject]@{ | |
| PSTypeName = 'Tls.Record' | |
| Direction = $Id | |
| ContentType = $buffer[0] | |
| Version = (([uint16]$buffer[1]) -shl 8) -bor $buffer[2] | |
| Length = $msgLength | |
| Data = [byte[]]($buffer[0..($msgLength + 4)]) | |
| } | |
| $MessageList.Add($msgObj) | |
| } | |
| } | |
| catch [System.OperationCanceledException] {} | |
| catch { | |
| [Console]::WriteLine("$Id Failed - $_") | |
| throw | |
| } | |
| } | |
| $cancelToken = [System.Threading.CancellationTokenSource]::new() | |
| $msgList = [System.Collections.Generic.List[PSObject]]::new() | |
| $backgroundTasks = @(if ($CapturePacket) { | |
| $psRead = [PowerShell]::Create() | |
| $psRead.AddScript($tlsPipeRecorder).AddParameters(@{ | |
| Id = "Outgoing" | |
| Incoming = $clientPipe | |
| Outgoing = $socketStream | |
| MessageList = $msgList | |
| CancelToken = $cancelToken.Token | |
| }).InvokeAsync() | |
| $psWrite = [PowerShell]::Create() | |
| $psWrite.AddScript($tlsPipeRecorder).AddParameters(@{ | |
| Id = "Incoming" | |
| Incoming = $socketStream | |
| Outgoing = $clientPipe | |
| MessageList = $msgList | |
| CancelToken = $cancelToken.Token | |
| }).InvokeAsync() | |
| }) | |
| $sslOptions = @{ | |
| EnabledSslProtocols = $Protocol | |
| TargetHost = $HostName | |
| } | |
| if ($CipherSuite) { | |
| $sslOptions.CipherSuitesPolicy = $CipherSuite | |
| } | |
| $certCallback = { | |
| [CmdletBinding()] | |
| param ( | |
| [object]$SenderObj, | |
| [System.Security.Cryptography.X509Certificates.X509Certificate]$Certificate, | |
| [System.Security.Cryptography.X509Certificates.X509Chain]$Chain, | |
| [System.Net.Security.SslPolicyErrors]$PolicyErrors | |
| ) | |
| $SenderObj._CertCallbackBag.Certificate = $Certificate | |
| $SenderObj._CertCallbackBag.Chain = $Chain | |
| $SenderObj._CertCallbackBag.PolicyErrors = $PolicyErrors | |
| $true | |
| } | |
| $targetStream = $CapturePacket ? $serverPipe : $socketStream | |
| $tlsStream = [System.Net.Security.SslStream]::new($targetStream, $true, $certCallback) | |
| # This is used in the callback to smuggle back the callback params | |
| $callbackInfo = @{} | |
| $tlsStream.PSObject.Properties.Add( | |
| [System.Management.Automation.PSNoteProperty]::new('_CertCallbackBag', $callbackInfo)) | |
| Write-Verbose "Starting TLS Handshake" | |
| try { | |
| $tlsStream.AuthenticateAsClient($sslOptions) | |
| Write-Verbose "TLS Handshake complete" | |
| } | |
| catch { | |
| # The exception here may contain an unhelpful see inner exception, | |
| # raise the inner exception to display a more helpful error. | |
| $exp = $_.Exception.InnerException | |
| if ($exp.Message -like "*see inner exception*") { | |
| $exp = $exp.InnerException | |
| } | |
| $err = [System.Management.Automation.ErrorRecord]::new( | |
| $exp, | |
| "TLSHandsakeFailure", | |
| [System.Management.Automation.ErrorCategory]::ProtocolError, | |
| $HostName) | |
| $err.ErrorDetails = "TLS Handshake Failure: $($exp.Message)" | |
| $PSCmdlet.WriteError($err) | |
| } | |
| finally { | |
| $cancelToken.Cancel() | |
| foreach ($task in $backgroundTasks) { | |
| while (-not $task.AsyncWaitHandle.WaitOne(200)) { } | |
| $task.GetAwaiter().GetResult() | |
| } | |
| } | |
| [PSCustomObject]@{ | |
| PSTypeName = 'Tls.Handshake' | |
| Protocol = $tlsStream.SslProtocol | |
| Cipher = $tlsStream.NegotiatedCipherSuite | |
| KeyStrength = $tlsStream.KeyExchangeStrength | |
| Certificate = $callbackInfo.Certificate | |
| Chain = $callbackInfo.Chain | |
| VerificationStatus = $callbackInfo.PolicyErrors | |
| Packets = $msgList | |
| } | |
| } | |
| finally { | |
| ${socketStream}?.Dispose() | |
| ${socket}?.Dispose() | |
| ${tlsStream}?.Dispose() | |
| ${clientPipe}?.Dispose() | |
| ${serverPipe}?.Dispose() | |
| } | |
| } |
According to the docs it’s been in dotnet since core 3.0
Core 3.0, Core 3.1, 5, 6, 7 RC 1
Is the error around missing types or is it an error that actually says this is not available on this platform. If it’s the latter I’ll have to find a way to get that working for Windows then.
Yes - that is the error but I suspect it is something environmental on my Windows resources.
Does this below look ok to you? Even on machines without error, I negotiate TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256
[System.Collections.Generic.List[System.Net.Security.TlsCipherSuite]] $ciphers = @()
$ciphers.Add([System.Net.Security.TlsCipherSuite]::TLS_ECDH_RSA_WITH_AES_256_CBC_SHA384)
$cipherSuite = [System.Net.Security.CipherSuitesPolicy]::new($ciphers)
Trace-TlsHandshake -HostName google.com -CipherSuite $cipherSuite
It seems sane to me, will have to try it out some more to test it though sorry.
I've had a further look and it seems like this is just not implemented on Windows with dotnet dotnet/runtime#23818.
Note: OpenSsl 1.1.1 or OSX is required. Windows is not supported at the moment.
I haven't seen any action for bringing support for this to Windows and even knowing the SSPI APIs to do so sounds a bit complex.
I'll just have to update the docstring to reflection this information.
What would be involved to make this work with a proxy? (basic auth authenticated ideally)
Guess have to do the CONNECT request etc.
Thanks for this great script -- are you aware if
CipherSuitesPolicyis available on Windows installations of Powershell 7+ ? So far, everywhere I have tried reports "CipherSuitesPolicy is not supported on this platform."Do you have an example call to this function passing in a cipher suite?