Skip to content

Instantly share code, notes, and snippets.

@akunzai
Last active May 19, 2025 18:48
Show Gist options
  • Save akunzai/8bbb8fff5e5988ddcbea60b97f28f596 to your computer and use it in GitHub Desktop.
Save akunzai/8bbb8fff5e5988ddcbea60b97f28f596 to your computer and use it in GitHub Desktop.
Uses OpenSSL to detect remote server support cipher suites
#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