Public/AzStackHci.SSLInspection.ps1

# ///////////////////////////////////////////////////////////////////
# Test-AzStackHciSSLInspection function
# Used to check for the presence of SSL Inspection on traffic sent to a specified URL
# ///////////////////////////////////////////////////////////////////
function Test-AzStackHciSSLInspection {
    <#
    .SYNOPSIS
    Function to check for the presence of SSL Inspection on traffic sent to a specified URL.
 
    .PARAMETER url
    The URL to test for SSL Inspection
 
    .DESCRIPTION
    Expects Microsoft or DigiCert certificates to be used for SSL/TLS connections to the specified URL.
    If a different certificate is detected, the script will report that SSL Inspection is present.
    Script checks for redirects and follows them to test further URLs if required.
    Returns $true if SSL Inspection is detected, otherwise $false.
    #>


    [CmdletBinding()]
    [OutputType([bool])]
    param (
        [parameter(Mandatory=$true,HelpMessage="The URL to test for SSL Inspection",Position=0)]
        [System.Uri]$url,

        [Parameter(Mandatory=$false, HelpMessage="Optional switch to prevent console output from the function.")]
        [switch]$NoOutput
    )

    begin {
        # Handle -NoOutput: suppress all console output
        if ($NoOutput.IsPresent) {
            $script:SilentMode = $true
            $VerbosePreference = 'SilentlyContinue'
            $DebugPreference = 'SilentlyContinue'
        }

        $ErrorActionPreference = "Stop"

        Write-HostAzS "`n`t///////////////////////////////////////////////"
        Write-HostAzS "`t Basic SSL Inspection Test for Azure Stack HCI"
        Write-HostAzS "`t///////////////////////////////////////////////`n"

        [bool]$RedirectsComplete = $false
        [bool]$SSLInspectionDetected = $false

        Write-HostAzS "Starting SSL Inspection Tests`n"

        Write-HostAzS "Date/Time = $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')"
        Write-HostAzS "Performing test from hostname: $($env:COMPUTERNAME)`n"
    }

    process {

        do {
            $Request = $null
            $Response = $null
            $Request = [System.Net.HttpWebRequest]::Create($url)
            $Request.Method = "GET"
            $Request.AllowAutoRedirect = $False
            $Request.Proxy = [System.Net.WebRequest]::DefaultWebProxy
            Write-HostAzS "Testing SSL/TLS Certificate for endpoint: '$($Request.Address.AbsoluteUri)'"
            try {
                [System.Net.HttpWebResponse]$Response = $Request.GetResponse()    
            }
            catch {
                Write-HostAzS "Error: $($_.Exception.Message)"
            }
            # Check if the certificate subject contains "O=Microsoft" as most SSL inspection appliances will replace the certificate with their own
            if(($Request.ServicePoint.Certificate.Subject).Contains("O=Microsoft")){
                Write-HostAzS -ForegroundColor Green "Expected Certificate Subject Found: `nSubject = $($Request.ServicePoint.Certificate.Subject)"
            } else {
                $SSLInspectionDetected = $true
                Write-HostAzS -ForegroundColor Red "UNKNOWN Certificate Subject Found: `nSubject = '$($Request.ServicePoint.Certificate.Subject)'"
                Write-HostAzS -ForegroundColor Yellow "`tNote: Expected Certificate Contains Subject = 'O=Microsoft'"
            }
            # Check if the certificate issuer contains "O=Microsoft Corporation" or "O=DigiCert Inc" as most SSL inspection appliances will replace the certificate with their own
            if(($Request.ServicePoint.Certificate.Issuer).Contains("O=Microsoft Corporation") -or (($Request.ServicePoint.Certificate.Issuer).Contains("O=DigiCert Inc"))){
                Write-HostAzS -ForegroundColor Green "Expected Certificate Issuer Found: `nCertificate Issuer = $($Request.ServicePoint.Certificate.Issuer)"
            } else {
                $SSLInspectionDetected = $true
                Write-HostAzS -ForegroundColor Red "UNKNOWN Certificate Issuer Found: `nCertificate Issuer = '$($Request.ServicePoint.Certificate.Issuer)'"
                Write-HostAzS -ForegroundColor Yellow "`tNote: Expected Certificate Contains Issuer = 'O=Microsoft Corporation' or 'O=DigiCert Inc'"
            }

            # Root CA thumbprint validation: build the certificate chain and verify the root
            # against a known list of trusted Microsoft/DigiCert root CA thumbprints.
            # This provides a stronger check than string matching alone — a sophisticated
            # MITM could spoof Issuer/Subject strings but not root CA thumbprints.
            try {
                $cert2 = New-Object System.Security.Cryptography.X509Certificates.X509Certificate2($Request.ServicePoint.Certificate)
                $chain = New-Object System.Security.Cryptography.X509Certificates.X509Chain
                $null = $chain.Build($cert2)
                $rootCert = $chain.ChainElements[$chain.ChainElements.Count - 1].Certificate
                if ($script:TRUSTED_ROOT_CA_THUMBPRINTS -contains $rootCert.Thumbprint) {
                    Write-HostAzS -ForegroundColor Green "Root CA Thumbprint Verified: $($rootCert.Thumbprint) ($($rootCert.Subject))"
                } else {
                    $SSLInspectionDetected = $true
                    Write-HostAzS -ForegroundColor Red "UNKNOWN Root CA Thumbprint: $($rootCert.Thumbprint)"
                    Write-HostAzS -ForegroundColor Red "Root CA Subject: $($rootCert.Subject)"
                    Write-HostAzS -ForegroundColor Yellow "`tNote: Root CA thumbprint does not match any known Microsoft/DigiCert root CAs"
                }
            } catch {
                Write-HostAzS -ForegroundColor Yellow "Warning: Unable to build certificate chain for root CA thumbprint validation: $($_.Exception.Message)"
            }

            # If $Response exists, check if for any redirects to further test required URLs
            if($Response){
                if(-not([string]::IsNullOrWhiteSpace($Response.Headers["Location"]))){
                    Write-HostAzS "Checking Redirected URL, as 'HTTP StatusCode = $($Response.StatusCode)'"
                    $url = $Response.Headers["Location"]
                } else {
                    # No redirects found
                    $RedirectsComplete = $true
                }
                $Response.Close()
            } else {
                # No response found, unable to determine if redirects are required
                $RedirectsComplete = $true
            }
            Write-HostAzS ""

        } while (
            $RedirectsComplete -ne $true
        ) # End of do..while loop for redirects
        
        # Note: this test uses both string matching AND root CA thumbprint verification.
        # String matching checks Subject/Issuer for known Microsoft/DigiCert organizations.
        # Thumbprint matching validates the root certificate against known trusted root CA thumbprints
        # from $script:TRUSTED_ROOT_CA_THUMBPRINTS (defined in AzStackHci.Constants.ps1).
        Write-HostAzS -ForegroundColor Yellow "Note: This test verifies certificates by matching Subject/Issuer strings and root CA thumbprints."
        Write-HostAzS -ForegroundColor Yellow "It is not intended to be an exhaustive certificate validity test.`n"

        if($SSLInspectionDetected -eq $true){
            Write-HostAzS -ForegroundColor Red "SSL Inspection Detected!`nCheck your network / proxy server configuration for SSL Inspection - https://learn.microsoft.com/en-us/azure-stack/hci/concepts/firewall-requirements`n"
        } else {
            Write-HostAzS -ForegroundColor Green "No SSL Inspection Detected :-)`n"
        }

    } # End of process block

    end {
        if ($NoOutput.IsPresent) { $script:SilentMode = $false }
        Write-HostAzS "SSL Inspection Tests Complete`n"
        # Write-Debug "Completed Test-AzStackHciSSLInspection function"
        return $SSLInspectionDetected
    }

} # End of Test-AzStackHciSSLInspection function