-
-
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
CipherSuitesPolicy
is 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?