Search code examples
c#powershellclasstranslate

Understanding the difference between a C# class and the equivalent in Powershell


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()

Solution

  • 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.