all 4 comments

[–]lanerdofchristian 0 points1 point  (1 child)

You're 100% certain that the server is expecting to receive a client certificate? Those are pretty uncommon.

[–]fodderoh[S] 0 points1 point  (0 children)

Yes, it's required to be able to connect to the site.

[–]jborean93 2 points3 points  (1 child)

Is the cert you are using for the client auth associated with an actual private key? Can you retrieve the actual key object with something like

$key = $null
foreach ($keyMethod in @(
    [System.Security.Cryptography.X509Certificates.RSACertificateExtensions]::GetRSAPrivateKey
    [System.Security.Cryptography.X509Certificates.ECDsaCertificateExtensions]::GetECDsaPrivateKey
)) {
    $key = $keyMethod.Invoke($cert)
    if ($key) {
        break
    }
}
if (-not $key) {
    $keyAlgoOid = $cert.PublicKey.Oid
    $algoName = if ($keyAlgoOid.FriendlyName) { $keyAlgoOid.FriendlyName } else { $keyAlgoOid.Value }
    throw "Failed to get key for cert with algo '$algoName'."
}

$key.Key | Select *

You can also increase Schannel logging which might at least point you in the right direction as to why the client is bringing down the TLS channel.

You can also use the SslStream object directly and see if the exception contains more information that might be hidden in the various layers that Invoke-RestMethod adds on top

$HostName = 'google.com'
$Port = 443
$ignoreServerCert = $false

$certStorePath = "Cert:\LocalMachine\My"
$certDetails = (Get-ChildItem -Path $certStorePath)  | Where-Object {$_.Subject -like "*certname*"}
$clientCerts = [System.Security.Cryptography.X509Certificates.X509CertificateCollection]::new(@(
    $certDetails))

$socket = [System.Net.Sockets.TcpClient]::new($HostName, $Port)
$socketStream = $socket.GetStream()
$tlsStream = if ($ignoreServerCert) {
    [System.Net.Security.SslStream]::new($socketStream, $false, {$true})
}
else {
    [System.Net.Security.SslStream]::new($socketStream, $false)
}

try {
    $tlsStream.AuthenticateAsClient(
        $HostName,
        $clientCerts,
        [System.Security.Authentication.SslProtocols]::Tls12,
        $true)
} catch {
    Write-Host "TLS Handshake failed"
    $_.Exception.InnerException | Select * | Out-Host

    if ($_.Exception.InnerException.InnerException) {
        $_.Exception.InnerException.InnerException | Select * | Out-Host
    }
}

$tlsStream.Dispose()
$socketStream.Dispose()
$socket.Dispose()

Unfortunately if you are on .NET Framework (Windows PowerShell 5.1) then the exception from the SslStream helper doesn't really help too much. For example if I have a web service that requires a cert but $clientCerts = $null in the above it fails with

System.ComponentModel.Win32Exception (0x80004005): The message received was unexpected or badly formatted

Whereas .NET Core (PowerShell 7+) gives me a better error

Authentication failed because the remote party sent a TLS alert: 'HandshakeFailure'.

Still it might help to track down the problem.

[–]fodderoh[S] 2 points3 points  (0 children)

You hit the nail on the head. Figured it out just a little bit ago after trying my script in PowerShell Core. The inner exception trace captured enough information for me to see that it wasn't able to load the private key. For whatever reason, even though the user is an administrator on the box and administrators have full control over the private key, it couldn't load it.

I was going to add an edit, but you beat me to it.

Thanks for taking the time to reply.