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