1

I'm having the usual scenario with using Invoke-WebRequest in environments that have to use Powershell 5.1 or older, where the self-signed certificate errors the cmdlet with 'Invoke-WebRequest : The underlying connection was closed: An unexpected error occurred on a send.'

The C# code from this post is the only code that works after testing all the other solutions online: Where to place RemoteCertificateValidationCallback?

$code = @"
public class SSLHandler
{
    public static System.Net.Security.RemoteCertificateValidationCallback GetSSLHandler()
    {

        return new System.Net.Security.RemoteCertificateValidationCallback((sender, certificate, chain, policyErrors) => { return true; });
    }
    
}
"@

Add-Type -TypeDefinition $code
#disable checks
[System.Net.ServicePointManager]::ServerCertificateValidationCallback = [SSLHandler]::GetSSLHandler()
#do the request
try
{
    invoke-WebRequest -Uri myurl -UseBasicParsing
} catch {
    # do something
} finally {
   #enable checks again
   [System.Net.ServicePointManager]::ServerCertificateValidationCallback = $null
}

I'm trying to understand why this C# class works in Powershell, but when I try to rewrite the class as a class in Powershell or as a function, it does not work when setting the [System.Net.ServicePointManager]::ServerCertificateValidationCallback to use the Powershell equivalent class.

This is my code:

#Works
$UnsafeWebRequest = @'
public class UnsafeWebRequest
{
    public static System.Net.Security.RemoteCertificateValidationCallback DangerousAcceptAnyServerCertificateValidator()
    {
        return new System.Net.Security.RemoteCertificateValidationCallback( (Sender, Certificate, Chain, PolicyErrors) => { return true; } );
    }   
}
'@

Add-Type -TypeDefinition $UnsafeWebRequest
[System.Net.ServicePointManager]::ServerCertificateValidationCallback = [UnsafeWebRequest]::DangerousAcceptAnyServerCertificateValidator()


#Does not work
class UnsafeWebRequest 
{
    static [System.Net.Security.RemoteCertificateValidationCallback] DangerousAcceptAnyServerCertificateValidator() 
    {
        return [System.Net.Security.RemoteCertificateValidationCallback]{
            param
            (
                [System.Object] $Sender,
                [System.Security.Cryptography.X509Certificates.X509Certificate] $X509Certificate,
                [System.Security.Cryptography.X509Certificates.X509Chain] $X509Chain,
                [System.Net.Security.SslPolicyErrors] $SslPolicyErrors
            ) 
            return $True
        }
    }
}

[System.Net.ServicePointManager]::ServerCertificateValidationCallback = [UnsafeWebRequest]::DangerousAcceptAnyServerCertificateValidator()

The C# code will ignore self-signed certificates in Invoke-WebRequest calls, but the Powershell one will just error out with 'The underlying connection was closed: An unexpected error occurred on a send.' and I can't understand why.

I've tried to write the powershell code in different ways like the C# code, but the only thing that works with [System.Net.ServicePointManager]::ServerCertificateValidationCallback is the C# class.

Edit. After the explanation from mklement0 I figured I'll broaden my search and found two other solutions that could be Powershell-native. One using the obsolete [System.Net.ServicePointManager]::CertificatePolicy and one using the [System.Net.Security.RemoteCertificateValidationCallback] as a [System.Linq.Expressions.Expression]::Lambda expression as a Powershell function.

Thanks @mklement0 & https://github.com/PowerShell/PowerShell/issues/17340

Solution 1:

function New-RemoteCertificateValidationCallbackHandler
{
    [CmdletBinding()]
    [OutputType( [System.Net.Security.RemoteCertificateValidationCallback] )]
    Param ()
    Begin
    {
        Add-Type -AssemblyName System.Net
    }
    Process
    {
        $LinqLambdaExpression = [System.Linq.Expressions.Expression]::Lambda(
            [System.Net.Security.RemoteCertificateValidationCallback],
            
            [System.Linq.Expressions.Expression]::Block(
                [System.Linq.Expressions.Expression]::Constant($True)
            ),
            
            [System.Linq.Expressions.ParameterExpression[]](
                [System.Linq.Expressions.Expression]::Variable([System.Object]),
                [System.Linq.Expressions.Expression]::Variable([System.Security.Cryptography.X509Certificates.X509Certificate]),
                [System.Linq.Expressions.Expression]::Variable([System.Security.Cryptography.X509Certificates.X509Chain]),
                [System.Linq.Expressions.Expression]::Variable([System.Net.Security.SslPolicyErrors])
            )
        )
    }
    End
    {
        $LinqLambdaExpression.Compile()
    }
}

[System.Net.ServicePointManager]::ServerCertificateValidationCallback = New-RemoteCertificateValidationCallbackHandler

Solution 2:

#CertificatePolicy is obsoleted for this type, please use ServerCertificateValidationCallback instead.
class TrustAllCertificatePolicy : System.Net.ICertificatePolicy
{
    [System.Boolean] CheckValidationResult (
        [System.Net.ServicePoint] $ServicePoint,
        [System.Security.Cryptography.X509Certificates.X509Certificate] $X509Certificate,
        [System.Net.WebRequest] $WebRequest,
        [System.Int32] $CertificateProblem
    )
    {
        return $True
    }
}

[System.Net.ServicePointManager]::CertificatePolicy = [TrustAllCertificatePolicy]::new()
  • You are making an HTTPS (secure with TLS) connection. The certificate is the encryption key that is used with TLS. TLS the server sends a certificate block with names of usable certificates. The client then looks up the name in the user stores to find a matching certificate. When client code includes a ServerCertificateValidationCallback then only the one certificate in the call back can be used. If this certificate is not in the list of names from server than connection will fail. The actual key is not transmitted. Only name. The certificate has to be loaded in both client and server. – jdweng Mar 19 '23 at 12:24

2 Answers2

0

If you change the

[System.Net.ServicePointManager]::ServerCertificateValidationCallback = [UnsafeWebRequest]::DangerousAcceptAnyServerCertificateValidator()

in your UnsafeWebRequest to

[System.Net.ServicePointManager]::ServerCertificateValidationCallback = [System.Net.Security.RemoteCertificateValidationCallback]([UnsafeWebRequest]::DangerousAcceptAnyServerCertificateValidator())

you should find this works.

Powershell cannot infer the delegate type while the c# lambda can.

gilliduck
  • 2,762
  • 2
  • 16
  • 32
  • If I cast the class to [System.Net.Security.RemoteCertificateValidationCallback] as you suggest it still errors out in Powershell. I'm doing a basic test against my local router with it's self-signed certificate: Invoke-WebRequest -Uri https://192.168.1.1, which work fine when using the C# example as the [System.Net.ServicePointManager]::ServerCertificateValidationCallback handler. I also though that since I'm returning in the class a [System.Net.Security.RemoteCertificateValidationCallback] type it would be the same as your suggestion, but my understanding of C# is very limited. – Kjell Computer Mar 19 '23 at 13:36
  • The method is `[System.Net.Security.RemoteCertificateValidationCallback]` -typed, so your `[System.Net.Security.RemoteCertificateValidationCallback]` cast is an unnecessary no-op, and therefore doesn't make any difference. Note that if the problem were that the delegate type isn't recognized, it is the _assignment_ that would fail, not a later call to `Invoke-WebRequest`. To put it another way: the fact that the assignment _succeeds_ implies that the delegate was properly recognized. The real problem is that the thread that calls the callback has no PowerShell runspace associated with it. – mklement0 Mar 19 '23 at 15:52
0

Whenever a PowerShell script block ({ ... }) or the method of a PowerShell class serves as a .NET delegate, whatever thread later calls the delegate must have a PowerShell runspace associated with it.

This is not guaranteed here, which is why using (a method of) a PowerShell class does not work in your case.[1]

You have two options:

  • Stick with a regular .NET class - as produced by on-demand compilation via Add-Type -TypeDefinition based on C# source code, as in your original attempt.

  • Use the helper code from this (archived) repo, which creates a wrapper around a script-block-based / PowerShell-class-method-based delegate that explicitly attaches a runspace to it.

    • This answer demonstrates its use.

    • However, since that also requires one-demand compilation via Add-Type -TypeDefinition, this is only worth doing if you need to repeatedly wrap various delegates this way.

    • Either way, you could avoid the once-per-session performance penalty you pay for Add-Type -TypeDefinition by instead creating a pre-compiled assembly that you load into the session with Add-Type -LiteralPath.


[1] When a thread calls a PowerShell-based delegate without a runspace, the following exception occurs, which indicates the problem:
There is no Runspace available to run scripts in this thread. You can provide one in the DefaultRunspace property of the System.Management.Automation.Runspaces.Runspace type.
However, the .NET APIs underlying Invoke-WebRequest turn this into the generic The underlying connection was closed: An unexpected error occurred on a send. you saw.

mklement0
  • 382,024
  • 64
  • 607
  • 775