Private/Get-AACTlsCertificate.ps1

function Get-AACTlsCertificate {
    <#
    .SYNOPSIS
        Reads the TLS certificate a host presents - its expiry, subject and
        issuer - with a TLS handshake of its own (no HTTP request, no
        credentials).
    .DESCRIPTION
        The certificate is taken whether or not it's trusted, so an expired
        or self-signed one is still reported. Returns @{ NotAfter; Subject;
        Issuer; Error }.
    #>

    [CmdletBinding()]
    [OutputType([hashtable])]
    param(
        [Parameter(Mandatory)]
        [string] $HostName,

        [int] $Port = 443,

        [int] $TimeoutSeconds = 10
    )

    $tcp = [System.Net.Sockets.TcpClient]::new()
    try {
        if (-not $tcp.ConnectAsync($HostName, $Port).Wait([TimeSpan]::FromSeconds($TimeoutSeconds))) { return @{ NotAfter = $null; Subject = ''; Issuer = ''; Error = "no answer within $TimeoutSeconds seconds" } }
        # Accept any certificate: it's read, not trusted. (Synchronous, so the
        # callback runs on this thread - a script block can't run on another.)
        $ssl = [System.Net.Security.SslStream]::new($tcp.GetStream(), $false, { param($TlsSender, $TlsCertificate, $TlsChain, $TlsErrors) $true })
        try {
            $ssl.ReadTimeout = $TimeoutSeconds * 1000
            $ssl.AuthenticateAsClient($HostName)
            $certificate = [System.Security.Cryptography.X509Certificates.X509Certificate2]::new($ssl.RemoteCertificate)
            @{ NotAfter = $certificate.NotAfter.ToUniversalTime(); Subject = $certificate.Subject; Issuer = $certificate.Issuer; Error = '' }
        }
        finally { $ssl.Dispose() }
    }
    catch { @{ NotAfter = $null; Subject = ''; Issuer = ''; Error = $_.Exception.GetBaseException().Message } }
    finally { $tcp.Dispose() }
}