-
Notifications
You must be signed in to change notification settings - Fork 8.1k
Add CertificateValidationScript Parameter to Web Cmdlets #4970
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add CertificateValidationScript Parameter to Web Cmdlets #4970
Conversation
f43b9c4 to
5b6d04a
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe add that it is ignored when -SkipCertificateCheck is present?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In that case, perhaps the two parameters should be in mutually exclusive parameter sets?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@rkeithhill We don't want these to be full mutually exclusive via parameter set. we want to support this usage:
$script = { <# some validation script #> }
$PSDefaultParameterValues['Invoke-RestMethod:CertificateValidationScript'] = $script
# All calls will use the validation script
Invoke-RestMethod https://unknonwnurl.com/
# But the user may want to override their normal validation for a certain Url
Invoke-RestMethod https://devsite.com/ -SkipCertificateCheckAlso, the Web Cmdlets have traditionally had a single parameter set and there are other parameters that are "exclusive" (i.e. -InFile and -Body). Introducing a parameter set here would be awkward.
@iSazonov I can update the summary, but other parameters with this kind of behavior do not call it out in their summaries. I believe these summaries are only visible when coding in C#, correct? Since these are PSCmdlet derived, you don't even see these summaries from within the same assembly. In which case, this is visible only when working in classes that derive from WebRequestPSCmdlet.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If Web Cmdlets have traditionally had a single parameter set I agree to follow the pattern.
My thought was that we should add the comment somewhere. The place is best. It's good for documenting code. Also we generate XML help files - I guess it can be used for creation of HTLM documentation.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If I'm not mistaken, the help files are generated from the PowerShell/PowerShell-Docs markdown? I can certainly say that I have never seen gets or sets the Body property (the summary for -Body) in anything. I certainly don't see any of the parameter summaries in any of the help xmls. The only place this shows up is the Microsoft.PowerShell.Commands.Utility.xml file and that has the same unhelpful summaries as the other parameters.
I will update it. I just think it wont be actually that helpful to anyone.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There's two types of help, there's the updatable help that exists in PowerShell-Docs and there's api reference docs that get generated from the assemblies. Any public api should be documented in code as the documentation is generated from that xml.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@SteveL-MSFT Is this the documentation you are referring to? https://msdn.microsoft.com/en-us/library/microsoft.powershell.commands.webrequestpscmdlet(v=vs.85).aspx If not, what exactly? I ask because the other summaries are very unhelpful and if these are being documented somewhere they could use some serious attention. I have never seen these summaries outside of the XML file and the source code.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We already added in our coding guigance that we welcome good comments everywhere. I hope in time we'll see a well-documented code 😄
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe put x509Certificate2on first position because the callback is for it?
Another thought is to pass a PSObject as $PSItem with the properties (x509Certificate2, httpRequestMessage, ...).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
All 4 are equally important for validating the certificate.
$Script = {
Return (
$HttpRequestMessage.RequestUri.AbsoluteUri -eq 'https://www.google.com/' -and
$X509Certificate2.Subject -eq 'CN=www.google.com, O=Google Inc, L=Mountain View, S=California, C=US' -and
$X509Chain.ChainElements[2].Certificate.Thumbprint -eq 'DE28F4A4FFE5B92FA3C503D1A349A7F9962A8212' -and
$SslPolicyErrors -eq 'None'
)
}I wanted to mirror the ,NET callback as much as possible to prevent confusion.
To be clear, $args[1], $X509Certificate2, and Param( [Parameter(Position=1)]$certificate <#...#> ) are all possible to the user.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I believe if we say about a script block we shouldn't concerned about .Net templates. Script writes don't see underlying implementations and usualy expect implicit $PSItem or explicit Param.
Also the variables actually is automatic variables and we should add its in list of automatic variables. This seems unjustified.
I suggest we wait for @lzybkr opinion.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's automatic to the ScriptBlock only. These do not show up in the calling scope. I don't see the problem? Please elaborate "we should add its in list of automatic variables". Do Script Block have their own list of automatic variables?
And, we should be concerned about the underlying .NET. It would be confusing to maintainers if the Func delegate has one argument order but the script block has another. The user gets the convenience of the automatic variables or Param() if they don't like that $args[0] is the HttpMessageRequest . As I said, all 4 arguments are equally important. Some users may have rules which never even test the certificate (URL based acceptance).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why set the variables and pass them as args?
Also, why set $ErrorActionPreference? That might not be desirable.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why set the variables and pass them as args?
I wanted to support parameter binding by type i.e.
param(
[System.Security.Cryptography.X509Certificates.X509Certificate2]$cert,
# ...I was trying to be flexible.
Also, why set
$ErrorActionPreference? That might not be desirable.
The [planned] documented behavior is that that errors and exceptions result in certificate failures. I ran into some strange scenarios where if errors weren't terminating it would hang the thread. but, this was before I switched to a closure and before I was using LanguagePrimitives.IsTrue(). I'm not sure if it s still needed, I can check though.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm definitely not in favor of both variables and parameters. Either way you need good documentation, but I prefer parameters.
And please do verify that setting $ErrorActionPreference is not needed.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is the draft documentation:
MicrosoftDocs/PowerShell-Docs@staging...markekraus:CertificatevalidationScriptDraft
I'm not sure I agree that parameters are preferable. Personally, I'm OK with either. But, I think most people would rather have auto-variables than have to declare param() or try to remember which $args is which. in the end, this is a documentation heavy dependent feature no matter which way it is implemented.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'll let the @PowerShell/powershell-committee chime in on this one.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Automatic variables removed per @PowerShell/powershell-committee decision. $ErrorActionPreference also appears to be no longer needed.
|
@lzybkr @SteveL-MSFT Is there something more I need to do for this PR? |
|
LGTM but we need review from MSFT experts. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd sleep better if this was in a finally - then I don't need to worry about a change to let some exceptions through.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I can do that.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
no longer applicable.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is worrisome. From reading just this code, I have no clue if currentRunspace will be blocked while the delegate is running.
For example, if we're streaming objects and in the middle of a call to WriteObject in the cmdlet's thread, that could yield to a random PowerShell event handler, which will be a race condition with whatever this delegate might do.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not sure how to make this work in a different runspace and supporting access to the current scope. To me, personally, that is not an issue as I can live with this not having scope access. But in the discussion the issue was brought up that users might find that confusing. or difficult to work with.
Can you mock up something that would cause this race condition so I can test it? I did some basic tests with this running in a pipeline and as part of a process block, but I was never able to cause a hang. or unexpected behavior. I thought maybe since we are forcing the call to run synchronously it might nor be an issue:
Line 468 in aea561f
| HttpResponseMessage response = client.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, _cancelToken.Token).GetAwaiter().GetResult(); |
But, honestly, I'm not anywhere near an expert on this area. I just know that if I don't specify a run space here I get the exception about a missing runspace.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sure, before running your code, run something like:
$t = [System.Timers.Timer]::new(10)
$t.AutoReset = $true
Register-ObjectEvent -InputObject $t -EventName Elapsed -Action { Write-Host 'event' }
$t.Enabled = $trueIf this doesn't cause problems, try something less trivial in the action - call functions, maybe a recursive function calculating fibonacci or something.
In your certificate validation script, also do something similarly non-trivial, e.g. compute a different fibonacci number.
I'm thinking some state will be corrupted, e.g. the current scope.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Using that event and a simple block caused it to hang the thread. So definitely some corruption.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
10ms might be too fast, maybe try a little slower, say 100ms. The goal was to hit a race as quickly as possible and get something other than a hang, though I suppose a hang is one possible outcome of a race.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@lzybkr actually... I think that test may kill any callback, not just the one in this issue.
I reduced the code to this
Func<HttpRequestMessage,X509Certificate2,X509Chain,SslPolicyErrors,bool> validationCallBackWrapper =
delegate(HttpRequestMessage httpRequestMessage, X509Certificate2 x509Certificate2, X509Chain x509Chain, SslPolicyErrors sslPolicyErrors)
{
Boolean result = false;
try
{
result = LanguagePrimitives.IsTrue(
CertificateValidationScript.InvokeReturnAsIs(
httpRequestMessage,
x509Certificate2,
x509Chain,
sslPolicyErrors
)
);
}
catch // Treat all exceptions as Certificate failures.
{
result = false;
}
return result;
};
handler.ServerCertificateCustomValidationCallback = validationCallBackWrapper;No more closure, no more setting the default runspace, no more auto variables or erroraction. Just wrapping the InvokeReturnAsIs() in the delegate and this hangs:
$t = [System.Timers.Timer]::new(10)
$t.AutoReset = $true
Register-ObjectEvent -InputObject $t -EventName Elapsed -Action { write-host = 'event' }
$t.Enabled = $true
$script = { return $true }
Invoke-RestMethod -uri https://httpbin.org/get -CertificateValidationScript $Scriptif I reduce it back to
Func<HttpRequestMessage,X509Certificate2,X509Chain,SslPolicyErrors,bool> validationCallBackWrapper =
LanguagePrimitives.ConvertTo<Func<HttpRequestMessage,X509Certificate2,X509Chain,SslPolicyErrors,bool>>(CertificateValidationScript);
handler.ServerCertificateCustomValidationCallback = validationCallBackWrapper;I get this:
Invoke-RestMethod : 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. The script block you attempted
to invoke was: return $true
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
100ms didn't make a difference.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This code has been changed and the setting/unsetting of the runspace is no longer a part of it. However, the callback still results in a thread hang when using the event trigger(s) provided by @lzybkr. I'm not sure where to go on that issue.
|
@PowerShell/powershell-committee reviewed the topic of params or automatic variables passed to the script block delegate. There was concerns about new automatic variables scoped only to this usage but would needed to be documented in the about_automaticvariables topic which may create some confusion. In addition, params seems to be a general pattern already in use across PowerShell, so the request is to use params for the script block (and update the doc draft accordingly). There were other concerns we didn't have time to discuss such as which runspace the delegate ran in. For now, continue that discussion in this PR. |
|
Well that's baffling. It's not a surprise that the certificate validation doesn't work on macOS. I ran into similar issues enabling
It should have failed before. |
@SteveL-MSFT Was the prospect of providing the callback parameters to the scriptblock by way of properties on I've been favoring Here's a mock-up of {
param($Message,$Cert,$Chain,$PolicyErrors)
Assert-CertificateCompliance -Policy Strict -Message $Message -Cert $Cert -Chain $Chain -PolicyErrors $PolicyErrors
}
{$_ | Assert-CertificateCompliance -Policy Strict}For me the point of this PR is to be able to impose per-URL certificate requirements. So I'll end up with policy data structures looking something like this: @{
Uri = 'https://github.com'
CertificateValidator = {
$_ | Assert-CertificateCompliance -Policy Strict
$_ | Assert-Certificate Server -Thumbprint d79f076110b39293e349ac89845b0380c19e2f8b
}
}
@{
Uri = 'https://www.powershellgallery.com'
CertificateValidator = {
$_ | Assert-CertificateCompliance -Policy Strict
$_ | Assert-Certificate Intermediate -Thumbprint 98af62baa639b0827f887c75a77f4afd4eff2685
}
}The validation scriptblock gets repeated for each uri so noise from |
|
@alx9r no, using members of |
|
@SteveL-MSFT I had an early draft that did it that way. I had dropped it in favor of the automatic variables before submitting the PR. |
|
@markekraus sorry, must have missed it. Getting myself involved in too many discussions on here to keep track of them (something I'm learning not to do!) |
|
@SteveL-MSFT I don't think the draft ever made it's way to my public repo so I don't think you missed anything, actually. My point was more that I'm on board with the idea of a |
f196c5f to
888b2b3
Compare
|
After much trial and error I determined that the only safe way to do this where any kind of event triggers were in place was run the I have incorporated the I have tested the current code with the event triggers @lzybkr provided and it works. no hangs. The down side is that the |
I'm curious what these tests looked like. It doesn't seem like
I see that the current documentation for
Is there a reason that the scope rules wouldn't be the same as for other scriptblocks using remote variables? FWIW, I think this would be a pretty awkward interface without having some way of making user objects available in the callback scriptblock. A natural use would be as follows: @{
u='https://github.com/some/important/file'
thumbprint = 'd79f076110b39293e349ac89845b0380c19e2f8b'
},
@{
u='https://powershellgallery.com/some/other/important/file'
thumbprint = '98af62baa639b0827f887c75a77f4afd4eff2685'
} |
% {
$thumbprint = $_.thumbprint
# ... do some prep things
Invoke-WebRequest $_.u -CertificateValidationScript {
# ... a bunch of tests that are identical for every certificate
$_.Certificate.Thumbprint -eq $using:thumbprint
}
# ... do some cleanup things
}Without |
I just used the same variable in @lzybkr's example and used it $t = [System.Timers.Timer]::new(10)
$t.AutoReset = $true
Register-ObjectEvent -InputObject $t -EventName Elapsed -Action { Write-Host 'event' }
$t.Enabled = $true
$script = {$t.Enabled = $false; $true}
irm -uri https://expired.badssl.com
#hangs
I disagree. Unless it is documented on this API, we should use something generic that is easier for PowerShell users to use.
Erm, not really? I mean yea, not on the ease of use level that using variables from the calling scope or I'm not saying this is ideal. What I am saying is that I can't see a safe way to get the calling scope available in the remote scope with out severely over-complicating the implementation. Also, these validations scripts are "per call". For 6.1 I want to look at adding session persistent options. One of them will be add your own .NET delegates for cert validation, So if you want to have some variable magic then you can. The goal in this PR is to provide the basic implementation that will work for most people on a "per call" basis. I think most will just need to ensure a static thumbprint or 2 against URLs. Complex validation scenarios are not the goal of the implementation in this PR, but are on my radar for the futre. |
OK. Got it. Thanks @markekraus |
888b2b3 to
bf8c9dc
Compare
|
rebased to fix merge conflict. |
|
@markekraus Another merge conflict. |
bf8c9dc to
9bece06
Compare
|
rebased. |
|
@PowerShell/powershell-committee reviewed this and recommends using the param() format. Specifically, $_ provides no context on what it represents. Typically $_ means the current object and reusing it in the scriptblock changes that expectation. Using param() makes the code more readable and therefore maintainable. |
|
@markekraus Please rebase with next new commit to get latest updates and pass CIs. |
|
@iSazonov I still need to incorporate the changes per the committee's decision. I will hopefully have free time this week. I will rebase before I commit the changes. |
|
FYI, I'm waiting on #5518 as this will need to rebased again and more drastically. |
|
Due to the issues of rebasing this and that the code still needs to be updated to fit the review comitee's decision, I'm going to close this PR. I will attempt to address this feature before 6.1.0. |
closes #4899
This adds a
-CertificateValidationScriptparameter toInvoke-RestMethodandInvoke-WebRequestwhich allows the user to supply a custom validation script as a PowerShellScriptBlockto validate Server Certificates for HTTPS connections.The ScriptBlock also has 4 automatic variables created which allows the user to use either
$args,Param(), or the automatic variables to inspect theHttpRequestMessagebeing sent to the server,X509Certificate2certificate returned by the server, theX509Chaincertificate chain of the server certificate, and theSslPolicyErrors. The ScriptBlock is converted to a Closure and is wrapped in aFuncdelegate which is used as theHttpClientHandler.ServerCertificateCustomValidationCallback. The ScriptBlock has access to the calling scope and any ScriptBlock exceptions are treated as a certificate failure.Documentation Needed
This new parameter will need to be documented and an example added. The documentation will need to note that the
$using:scope is not supported and will result in a certificate failure. it will also need to note that if the site redirects, the same validation script will apply for all hops. Finally, it will also need to state that-SkipCertificateCheckwill override anything supplied in-CertificateValidationScript.