Last active
May 19, 2025 18:48
-
-
Save akunzai/8bbb8fff5e5988ddcbea60b97f28f596 to your computer and use it in GitHub Desktop.
Uses OpenSSL to detect remote server support cipher suites
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#Requires -Version 5.1 | |
[CmdletBinding()] | |
param ( | |
[Parameter(Mandatory=$true)] | |
[string]$RemoteAddress, | |
[Parameter(Mandatory=$false)] | |
[string]$OpenSslPath = 'openssl', | |
[Parameter(Mandatory=$false)] | |
[ValidateSet('AllKnown', 'ClientSupported')] | |
[string]$CipherListSource = 'ClientSupported', | |
[Parameter(Mandatory=$false)] | |
[string[]]$TlsVersion = @('1.2'), # Can be '1.0', '1.1', '1.2', '1.3' | |
[Parameter(Mandatory=$false)] | |
[double]$DelaySeconds = 0.1 | |
) | |
$TargetHost = $null | |
$TargetPort = 443 | |
if ($RemoteAddress.Contains(':')) { | |
$parts = $RemoteAddress.Split(':', 2) | |
$TargetHost = $parts[0] | |
if ($parts.Length -gt 1 -and -not [string]::IsNullOrWhiteSpace($parts[1])) { | |
try { | |
$TargetPort = [int]$parts[1] | |
if ($TargetPort -lt 1 -or $TargetPort -gt 65535) { | |
Write-Error "Invalid port number specified in RemoteAddress: $($parts[1]). Port must be between 1 and 65535." | |
exit 1 | |
} | |
} catch { | |
Write-Error "Invalid port number specified in RemoteAddress: $($parts[1]). Must be a valid integer." | |
exit 1 | |
} | |
} | |
} else { | |
$TargetHost = $RemoteAddress | |
} | |
if ([string]::IsNullOrWhiteSpace($TargetHost)) { | |
Write-Error "Could not determine a valid hostname from RemoteAddress: '$($RemoteAddress)'" | |
exit 1 | |
} | |
try { | |
$null = (& $OpenSslPath version 2>&1) | |
if ($LASTEXITCODE -ne 0) { | |
$errorOutput = (& $OpenSslPath version 2>&1 | Out-String) | |
Write-Error "OpenSSL command '$OpenSslPath' failed or is not found. Please ensure OpenSSL is installed and in your PATH, or specify the correct path via -OpenSslPath. Output/Error: $($errorOutput). Exit code: $LASTEXITCODE" | |
exit 1 | |
} | |
} catch { | |
Write-Error "Exception while trying to execute '$OpenSslPath version': $($_.Exception.Message)" | |
Write-Error "Please ensure OpenSSL is installed and in your PATH, or specify the correct path via -OpenSslPath." | |
exit 1 | |
} | |
# Determine the effective cipher list source, allowing for fallback | |
$effectiveCipherListSource = $CipherListSource | |
$clientSupportedCiphers = @() # Will hold ciphers if ClientSupported is successful | |
if ($CipherListSource -eq 'ClientSupported') { # User *requested* ClientSupported | |
Write-Host "Attempting to gather cipher suites from OS (ClientSupported mode)..." | |
$getTlsCipherSuiteCmd = Get-Command -Name Get-TlsCipherSuite -ErrorAction SilentlyContinue | |
if (-not $getTlsCipherSuiteCmd) { | |
Write-Warning "Cmdlet 'Get-TlsCipherSuite' not found. Falling back to 'AllKnown' mode for cipher list source." | |
$effectiveCipherListSource = 'AllKnown' | |
} else { | |
# Cmdlet exists, try to use it and convert ciphers | |
$opensslNamesFromOS = [System.Collections.Generic.List[string]]::new() | |
$uniqueConvertedNames = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::OrdinalIgnoreCase) | |
$anyOsCiphersFoundByCmdlet = $false # Flag to track if Get-TlsCipherSuite returned anything | |
try { | |
$osCipherSuites = Get-TlsCipherSuite | Where-Object { $_.Name -notlike "TLS_PSEUDO_*" } | |
if ($null -eq $osCipherSuites -or $osCipherSuites.Count -eq 0) { | |
Write-Warning "Get-TlsCipherSuite did not return any applicable cipher suites on this system." | |
# Fallback will occur later if $opensslNamesFromOS remains empty | |
} else { | |
$anyOsCiphersFoundByCmdlet = $true | |
foreach ($osCipher in $osCipherSuites) { | |
$ianaName = $osCipher.Name | |
if ($ianaName -in ('TLS_ECDHE_PSK_WITH_AES_128_GCM_SHA256', 'TLS_ECDHE_PSK_WITH_AES_256_GCM_SHA384', 'TLS_DHE_PSK_WITH_AES_128_GCM_SHA256', 'TLS_DHE_PSK_WITH_AES_256_GCM_SHA384')) { | |
Write-Verbose "Skipping PSK-based cipher suite '$($ianaName)' as it typically requires specific pre-shared key setup not handled by this script." | |
continue | |
} | |
$convertOutputForWarning = '' | |
$convertedName = '' | |
try { | |
$convertOutputLines = @(& $OpenSslPath ciphers -convert $ianaName 2>&1) | |
if ($LASTEXITCODE -eq 0 -and $null -ne $convertOutputLines -and $convertOutputLines.Count -gt 0) { | |
$firstLineOutput = $convertOutputLines[0] | |
if ($firstLineOutput -notmatch 'Error' -and $firstLineOutput -notmatch 'unknown option' -and -not [string]::IsNullOrWhiteSpace($firstLineOutput)) { | |
$convertedName = ($firstLineOutput -replace '^OpenSSL cipher name: ', '').Trim() | |
} else { | |
$convertOutputForWarning = $convertOutputLines -join '; ' | |
} | |
} else { | |
$convertOutputForWarning = "OpenSSL exit code $LASTEXITCODE. Output: $($convertOutputLines -join '; ')" | |
} | |
} catch { | |
Write-Warning "Exception while executing 'openssl ciphers -convert $($ianaName)': $($_.Exception.Message)" | |
continue # Skip this cipher on conversion error | |
} | |
if (-not [string]::IsNullOrWhiteSpace($convertedName) -and $convertedName -notmatch 'unknown cipher') { | |
if ($uniqueConvertedNames.Add($convertedName)) { | |
$opensslNamesFromOS.Add($convertedName) | |
} | |
} else { | |
Write-Warning "Could not convert OS cipher suite '$($ianaName)' to OpenSSL name, or conversion resulted in an invalid name. OpenSSL output/status: $($convertOutputForWarning)" | |
} | |
} # End foreach $osCipher | |
} | |
$clientSupportedCiphers = $opensslNamesFromOS.ToArray() # Store the successfully converted ciphers | |
if ($clientSupportedCiphers.Count -eq 0) { | |
if ($anyOsCiphersFoundByCmdlet) { | |
Write-Warning "Found OS ciphers via Get-TlsCipherSuite, but failed to convert any to usable OpenSSL names." | |
} # Else: previous warning about Get-TlsCipherSuite returning nothing was already issued. | |
Write-Warning "Falling back to 'AllKnown' mode due to no usable ciphers from 'ClientSupported' mode." | |
$effectiveCipherListSource = 'AllKnown' | |
} else { | |
Write-Host "Successfully gathered $($clientSupportedCiphers.Count) unique cipher suites using 'ClientSupported' mode." | |
} | |
} catch { # Catch errors from Get-TlsCipherSuite itself or other unexpected issues in the try block | |
Write-Error "Error during Get-TlsCipherSuite execution or initial conversion phase: $($_.Exception.Message)" | |
Write-Warning "Falling back to 'AllKnown' mode due to error in 'ClientSupported' processing." | |
$effectiveCipherListSource = 'AllKnown' | |
} | |
} | |
} | |
function Test-TlsCipherSuiteHandshake { | |
[CmdletBinding()] | |
param ( | |
[Parameter(Mandatory=$true)] | |
[string]$HostName, | |
[Parameter(Mandatory=$true)] | |
[int]$Port, | |
[Parameter(Mandatory=$true)] | |
[string]$Cipher, | |
[Parameter(Mandatory=$true)] | |
[string]$SslPath, | |
[Parameter(Mandatory=$true)] | |
[string]$CurrentTlsVersion # "1.2", "1.3" | |
) | |
$commandOutput = '' | |
$status = 'FAILED' | |
$tlsCliOption = "-tls$($CurrentTlsVersion.Replace('.', '_'))" # -tls1_2, -tls1_3 | |
$cipherCliOptionName = if ($CurrentTlsVersion -eq '1.3') { '-ciphersuites' } else { '-cipher' } | |
try { | |
$commandOutput = (Write-Output "Q`n" | & $SslPath s_client -connect "$HostName`:$Port" $cipherCliOptionName $Cipher -servername $HostName $tlsCliOption 2>&1) | |
$escapedCipher = [regex]::Escape($Cipher) | |
foreach ($line in $commandOutput) { | |
if ($CurrentTlsVersion -eq '1.3') { | |
if ($line -match "^\s*Cipher\s+:\s+$escapedCipher\s*$") { | |
$status = 'OK'; break | |
} | |
} else { # TLS 1.2 and older | |
if ($line -match "^\s*Cipher is $escapedCipher\s*$") { | |
$status = 'OK'; break | |
} elseif ($line -match "^\s*Cipher\s+:\s+$escapedCipher\s*$") { | |
$status = 'OK'; break | |
} | |
} | |
} | |
if ($status -ne 'OK') { | |
$outputStringForVerbose = $commandOutput -join "`n" | |
if ($outputStringForVerbose -match "unsupported protocol|no ciphers available|wrong cipher returned|handshake failure|ssl alert number") { | |
Write-Verbose "Handshake failure or specific error detected for $Cipher on TLS $CurrentTlsVersion. OpenSSL output snippet: $($outputStringForVerbose | Select-String -Pattern 'alert', 'error', 'failure' -CaseSensitive -Quiet -Context 0,1)" | |
} else { | |
Write-Verbose "Cipher $Cipher not confirmed for TLS $CurrentTlsVersion. Full OpenSSL output for details if needed." | |
} | |
} | |
} catch { | |
$errorMessage = $_.Exception.Message | |
Write-Warning "Exception while executing openssl s_client for cipher $($Cipher) on TLS $($CurrentTlsVersion): $($errorMessage)" | |
} | |
return [PSCustomObject]@{ | |
CipherSuite = $Cipher | |
Status = $status | |
TlsVersion = $CurrentTlsVersion | |
} | |
} | |
Write-Host "Starting TLS cipher suite testing against $($TargetHost):$($TargetPort)..." | |
Write-Host "Specified TLS versions to test: $($TlsVersion -join ', ')" | |
Write-Host "Effective cipher list source: $effectiveCipherListSource" | |
if ($CipherListSource -ne $effectiveCipherListSource) { | |
Write-Host "(Note: Original CipherListSource was '$CipherListSource', but fell back to '$effectiveCipherListSource')" | |
} | |
if ($DelaySeconds -gt 0) { | |
Write-Host "Delay between tests: $($DelaySeconds)s" | |
} | |
foreach ($currentTlsVer in $TlsVersion) { | |
Write-Host "`n--- Testing for TLS Version: $currentTlsVer ---" | |
$ciphersForThisTlsVersion = @() | |
if ($effectiveCipherListSource -eq 'AllKnown') { | |
$OpenSslTlsCliOption = "-tls$($currentTlsVer.Replace('.', '_'))" # -tls1_2, -tls1_3 | |
$opensslCiphersCmd = "$OpenSslPath ciphers $OpenSslTlsCliOption -s 'ALL:eNULL'" | |
Write-Verbose "Getting ciphers for TLS $currentTlsVer using: $opensslCiphersCmd (Mode: $effectiveCipherListSource)" | |
$cipherListString = '' | |
try { | |
$cipherListString = (& $OpenSslPath ciphers $OpenSslTlsCliOption -s 'ALL:eNULL' 2>&1) | |
if ($LASTEXITCODE -ne 0 -or [string]::IsNullOrWhiteSpace($cipherListString) -or $cipherListString -match 'Error' -or $cipherListString -match 'Unknown option' -or $cipherListString -match 'Cipher string too short') { | |
Write-Warning "Failed to get cipher suite list from OpenSSL for TLS $currentTlsVer (Mode: $effectiveCipherListSource). Command: '$opensslCiphersCmd'. Output: $($cipherListString)" | |
} else { | |
$ciphersForThisTlsVersion = $cipherListString.Split(':') | Where-Object { -not [string]::IsNullOrWhiteSpace($_) } | |
} | |
} catch { | |
Write-Warning "Error executing '$($opensslCiphersCmd)' (Mode: $effectiveCipherListSource): $($_.Exception.Message)" | |
} | |
if ($ciphersForThisTlsVersion.Count -eq 0) { | |
Write-Warning "Failed to get any cipher suites from OpenSSL for TLS $currentTlsVer using '$opensslCiphersCmd' (Mode: $effectiveCipherListSource)." | |
} | |
} else { | |
$ciphersForThisTlsVersion = $clientSupportedCiphers | |
Write-Verbose "Using $($clientSupportedCiphers.Count) ciphers from 'ClientSupported' list for TLS $currentTlsVer." | |
} | |
if ($ciphersForThisTlsVersion.Count -eq 0) { | |
Write-Warning "No cipher suites available for testing for TLS Version $currentTlsVer (Source: $effectiveCipherListSource)." | |
continue | |
} | |
Write-Host "Testing $($ciphersForThisTlsVersion.Count) cipher suites for TLS $currentTlsVer (Source: $effectiveCipherListSource)..." | |
foreach ($cipherNameInOpenSSLFormat in $ciphersForThisTlsVersion) { | |
$cleanCipherName = $cipherNameInOpenSSLFormat.Trim() | |
if ([string]::IsNullOrWhiteSpace($cleanCipherName)) { | |
continue | |
} | |
$result = Test-TlsCipherSuiteHandshake -HostName $TargetHost -Port $TargetPort -Cipher $cleanCipherName -SslPath $OpenSslPath -CurrentTlsVersion $currentTlsVer | |
$statusColor = if ($result.Status -eq 'OK') { 'Green' } else { 'Red' } | |
Write-Host ("{0,-10} | {1,-50} : " -f "TLS $($result.TlsVersion)", $result.CipherSuite) -NoNewline | |
Write-Host $result.Status -ForegroundColor $statusColor | |
if ($DelaySeconds -gt 0 -and $DelaySeconds -lt 600) { | |
Start-Sleep -Seconds $DelaySeconds | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment