diff --git a/src/Microsoft.PowerShell.Commands.Utility/commands/utility/WebCmdlet/Common/WebRequestPSCmdlet.Common.cs b/src/Microsoft.PowerShell.Commands.Utility/commands/utility/WebCmdlet/Common/WebRequestPSCmdlet.Common.cs index 00828fec4b9..a79e40f492c 100644 --- a/src/Microsoft.PowerShell.Commands.Utility/commands/utility/WebCmdlet/Common/WebRequestPSCmdlet.Common.cs +++ b/src/Microsoft.PowerShell.Commands.Utility/commands/utility/WebCmdlet/Common/WebRequestPSCmdlet.Common.cs @@ -11,6 +11,7 @@ using System.Collections; using System.Globalization; using System.Security; +using System.Security.Authentication; using System.Security.Cryptography; using System.Security.Cryptography.X509Certificates; #if !CORECLR @@ -46,6 +47,35 @@ public enum WebAuthenticationType OAuth, } + // WebSslProtocol is used because not all SslProtocols are supported by HttpClientHandler. + // Also SslProtocols.Default is not the "default" for HttpClientHandler as SslProtocols.Ssl3 is not supported. + /// + /// The valid values for the -SslProtocol parameter for Invoke-RestMethod and Invoke-WebRequest + /// + [Flags] + public enum WebSslProtocol + { + /// + /// No SSL protocol will be set and the system defaults will be used. + /// + Default = 0, + + /// + /// Specifies the TLS 1.0 security protocol. The TLS protocol is defined in IETF RFC 2246. + /// + Tls = SslProtocols.Tls, + + /// + /// Specifies the TLS 1.1 security protocol. The TLS protocol is defined in IETF RFC 4346. + /// + Tls11 = SslProtocols.Tls11, + + /// + /// Specifies the TLS 1.2 security protocol. The TLS protocol is defined in IETF RFC 5246 + /// + Tls12 = SslProtocols.Tls12 + } + /// /// Base class for Invoke-RestMethod and Invoke-WebRequest commands. /// @@ -137,6 +167,12 @@ public abstract partial class WebRequestPSCmdlet : PSCmdlet [Parameter] public virtual SwitchParameter SkipCertificateCheck { get; set; } + /// + /// Gets or sets the TLS/SSL protocol used by the Web Cmdlet + /// + [Parameter] + public virtual WebSslProtocol SslProtocol { get; set; } = WebSslProtocol.Default; + /// /// Gets or sets the Token property. Token is required by Authentication OAuth and Bearer. /// diff --git a/src/Microsoft.PowerShell.Commands.Utility/commands/utility/WebCmdlet/CoreCLR/WebRequestPSCmdlet.CoreClr.cs b/src/Microsoft.PowerShell.Commands.Utility/commands/utility/WebCmdlet/CoreCLR/WebRequestPSCmdlet.CoreClr.cs index f5322ddc129..630f66804c6 100644 --- a/src/Microsoft.PowerShell.Commands.Utility/commands/utility/WebCmdlet/CoreCLR/WebRequestPSCmdlet.CoreClr.cs +++ b/src/Microsoft.PowerShell.Commands.Utility/commands/utility/WebCmdlet/CoreCLR/WebRequestPSCmdlet.CoreClr.cs @@ -13,6 +13,7 @@ using System.Text; using System.Collections; using System.Globalization; +using System.Security.Authentication; using System.Security.Cryptography; using System.Threading; using System.Xml; @@ -192,6 +193,9 @@ internal virtual HttpClient GetHttpClient(bool handleRedirect) } } + handler.SslProtocols = (SslProtocols)SslProtocol; + + HttpClient httpClient = new HttpClient(handler); // check timeout setting (in seconds instead of milliseconds as in HttpWebRequest) diff --git a/test/powershell/Modules/Microsoft.PowerShell.Utility/WebCmdlets.Tests.ps1 b/test/powershell/Modules/Microsoft.PowerShell.Utility/WebCmdlets.Tests.ps1 index 8dc1b1aaaef..fa5b15e8c24 100644 --- a/test/powershell/Modules/Microsoft.PowerShell.Utility/WebCmdlets.Tests.ps1 +++ b/test/powershell/Modules/Microsoft.PowerShell.Utility/WebCmdlets.Tests.ps1 @@ -1414,6 +1414,70 @@ Describe "Invoke-WebRequest tests" -Tags "Feature" { } } + Context "Invoke-WebRequest -SslProtocol Test" { + It "Verifies Invoke-WebRequest -SslProtocol works on " -TestCases @( + @{SslProtocol = 'Default'; ActualProtocol = 'Default'} + @{SslProtocol = 'Tls'; ActualProtocol = 'Tls'} + @{SslProtocol = 'Tls11'; ActualProtocol = 'Tls11'} + @{SslProtocol = 'Tls12'; ActualProtocol = 'Tls12'} + # macOS does not support multiple SslProtocols + if (-not $IsMacOS) + { + @{SslProtocol = 'Tls, Tls11, Tls12'; ActualProtocol = 'Tls12'} + @{SslProtocol = 'Tls11, Tls12'; ActualProtocol = 'Tls12'} + @{SslProtocol = 'Tls, Tls11, Tls12'; ActualProtocol = 'Tls11'} + @{SslProtocol = 'Tls11, Tls12'; ActualProtocol = 'Tls11'} + @{SslProtocol = 'Tls, Tls11'; ActualProtocol = 'Tls11'} + @{SslProtocol = 'Tls, Tls11, Tls12'; ActualProtocol = 'Tls'} + @{SslProtocol = 'Tls, Tls11'; ActualProtocol = 'Tls'} + @{SslProtocol = 'Tls, Tls12'; ActualProtocol = 'Tls'} + } + # macOS does not support multiple SslProtocols and possible CoreFX for this combo on Linux + if($IsWindows) + { + + @{SslProtocol = 'Tls, Tls12'; ActualProtocol = 'Tls12'} + } + ) { + param($SslProtocol, $ActualProtocol) + $params = @{ + Uri = Get-WebListenerUrl -Test 'Get' -Https -SslProtocol $ActualProtocol + SslProtocol = $SslProtocol + SkipCertificateCheck = $true + } + $response = Invoke-WebRequest @params + $result = $Response.Content | ConvertFrom-Json + + $result.headers.Host | Should Be $params.Uri.Authority + } + + It "Verifies Invoke-WebRequest -SslProtocol -SslProtocol fails on a only connection" -TestCases @( + @{IntendedProtocol = 'Tls'; ActualProtocol = 'Tls12'} + @{IntendedProtocol = 'Tls'; ActualProtocol = 'Tls11'} + @{IntendedProtocol = 'Tls11'; ActualProtocol = 'Tls12'} + @{IntendedProtocol = 'Tls11'; ActualProtocol = 'Tls'} + @{IntendedProtocol = 'Tls12'; ActualProtocol = 'Tls'} + @{IntendedProtocol = 'Tls12'; ActualProtocol = 'Tls11'} + # macOS does not support multiple SslProtocols + if (-not $IsMacOS) + { + @{IntendedProtocol = 'Tls11, Tls12'; ActualProtocol = 'Tls'} + @{IntendedProtocol = 'Tls, Tls12'; ActualProtocol = 'Tls11'} + @{IntendedProtocol = 'Tls, Tls11'; ActualProtocol = 'Tls12'} + } + ) { + param( $IntendedProtocol, $ActualProtocol) + $params = @{ + Uri = Get-WebListenerUrl -Test 'Get' -Https -SslProtocol $ActualProtocol + SslProtocol = $IntendedProtocol + SkipCertificateCheck = $true + ErrorAction = 'Stop' + } + { Invoke-WebRequest @params } | ShouldBeErrorId 'WebCmdletWebResponseException,Microsoft.PowerShell.Commands.InvokeWebRequestCommand' + } + + } + BeforeEach { if ($env:http_proxy) { $savedHttpProxy = $env:http_proxy @@ -2332,6 +2396,69 @@ Describe "Invoke-RestMethod tests" -Tags "Feature" { } } + Context "Invoke-RestMethod -SslProtocol Test" { + It "Verifies Invoke-RestMethod -SslProtocol works on " -TestCases @( + @{SslProtocol = 'Default'; ActualProtocol = 'Default'} + @{SslProtocol = 'Tls'; ActualProtocol = 'Tls'} + @{SslProtocol = 'Tls11'; ActualProtocol = 'Tls11'} + @{SslProtocol = 'Tls12'; ActualProtocol = 'Tls12'} + # macOS does not support multiple SslProtocols + if (-not $IsMacOS) + { + @{SslProtocol = 'Tls, Tls11, Tls12'; ActualProtocol = 'Tls12'} + @{SslProtocol = 'Tls11, Tls12'; ActualProtocol = 'Tls12'} + @{SslProtocol = 'Tls, Tls11, Tls12'; ActualProtocol = 'Tls11'} + @{SslProtocol = 'Tls11, Tls12'; ActualProtocol = 'Tls11'} + @{SslProtocol = 'Tls, Tls11'; ActualProtocol = 'Tls11'} + @{SslProtocol = 'Tls, Tls11, Tls12'; ActualProtocol = 'Tls'} + @{SslProtocol = 'Tls, Tls11'; ActualProtocol = 'Tls'} + @{SslProtocol = 'Tls, Tls12'; ActualProtocol = 'Tls'} + } + # macOS does not support multiple SslProtocols and possible CoreFX for this combo on Linux + if($IsWindows) + { + @{SslProtocol = 'Tls, Tls12'; ActualProtocol = 'Tls12'} + } + ) { + param($SslProtocol, $ActualProtocol) + $params = @{ + Uri = Get-WebListenerUrl -Test 'Get' -Https -SslProtocol $ActualProtocol + SslProtocol = $SslProtocol + SkipCertificateCheck = $true + } + $result = Invoke-RestMethod @params + + $result.headers.Host | Should Be $params.Uri.Authority + } + + It "Verifies Invoke-RestMethod -SslProtocol fails on a only connection" -TestCases @( + @{IntendedProtocol = 'Tls'; ActualProtocol = 'Tls12'} + @{IntendedProtocol = 'Tls'; ActualProtocol = 'Tls11'} + @{IntendedProtocol = 'Tls11'; ActualProtocol = 'Tls12'} + @{IntendedProtocol = 'Tls11'; ActualProtocol = 'Tls'} + @{IntendedProtocol = 'Tls12'; ActualProtocol = 'Tls'} + @{IntendedProtocol = 'Tls12'; ActualProtocol = 'Tls11'} + # macOS does not support multiple SslProtocols + if (-not $IsMacOS) + { + @{IntendedProtocol = 'Tls11, Tls12'; ActualProtocol = 'Tls'} + @{IntendedProtocol = 'Tls, Tls12'; ActualProtocol = 'Tls11'} + @{IntendedProtocol = 'Tls, Tls11'; ActualProtocol = 'Tls12'} + } + ) { + param( $IntendedProtocol, $ActualProtocol) + $params = @{ + Uri = Get-WebListenerUrl -Test 'Get' -Https -SslProtocol $ActualProtocol + SslProtocol = $IntendedProtocol + SkipCertificateCheck = $true + ErrorAction = 'Stop' + } + { Invoke-RestMethod @params } | ShouldBeErrorId 'WebCmdletWebResponseException,Microsoft.PowerShell.Commands.InvokeRestMethodCommand' + } + + + } + BeforeEach { if ($env:http_proxy) { $savedHttpProxy = $env:http_proxy diff --git a/test/tools/Modules/WebListener/README.md b/test/tools/Modules/WebListener/README.md index f66d94b2c16..0bb257d8f09 100644 --- a/test/tools/Modules/WebListener/README.md +++ b/test/tools/Modules/WebListener/README.md @@ -1,13 +1,13 @@ # WebListener Module -A PowerShell module for managing the WebListener App. The included SelF-Signed Certificate `ServerCert.pfx` has the password set to `password` and is issued for the Client and Server Authentication key usages. This certificate is used by the WebListener App for SSL/TLS. The included SelF-Signed Certificate `ClientCert.pfx` has the password set to `password` and has not been issued for any specific key usage. This Certificate is used for Client Certificate Authentication with the WebListener App. +A PowerShell module for managing the WebListener App. The included SelF-Signed Certificate `ServerCert.pfx` has the password set to `password` and is issued for the Client and Server Authentication key usages. This certificate is used by the WebListener App for SSL/TLS. The included SelF-Signed Certificate `ClientCert.pfx` has the password set to `password` and has not been issued for any specific key usage. This Certificate is used for Client Certificate Authentication with the WebListener App. The port used for `-HttpsPort` will use TLS 1.2. # Running WebListener ```powershell Import-Module .\build.psm1 Publish-PSTestTools -$Listener = Start-WebListener -HttpPort 8083 -HttpsPort 8084 +$Listener = Start-WebListener -HttpPort 8083 -HttpsPort 8084 -Tls11Port 8085 -TlsPort 8086 ``` # Stopping WebListener diff --git a/test/tools/Modules/WebListener/WebListener.psm1 b/test/tools/Modules/WebListener/WebListener.psm1 index a3125d07c6b..1e4ea444bf2 100644 --- a/test/tools/Modules/WebListener/WebListener.psm1 +++ b/test/tools/Modules/WebListener/WebListener.psm1 @@ -2,6 +2,8 @@ Class WebListener { [int]$HttpPort [int]$HttpsPort + [int]$Tls11Port + [int]$TlsPort [System.Management.Automation.Job]$Job WebListener () { } @@ -36,7 +38,13 @@ function Start-WebListener [int]$HttpPort = 8083, [ValidateRange(1,65535)] - [int]$HttpsPort = 8084 + [int]$HttpsPort = 8084, + + [ValidateRange(1,65535)] + [int]$Tls11Port = 8085, + + [ValidateRange(1,65535)] + [int]$TlsPort = 8086 ) process @@ -58,11 +66,13 @@ function Start-WebListener $Job = Start-Job { $path = Split-Path -parent (get-command WebListener).Path Push-Location $path - dotnet $using:appDll $using:serverPfxPath $using:serverPfxPassword $using:HttpPort $using:HttpsPort + dotnet $using:appDll $using:serverPfxPath $using:serverPfxPassword $using:HttpPort $using:HttpsPort $using:Tls11Port $using:TlsPort } $Script:WebListener = [WebListener]@{ HttpPort = $HttpPort HttpsPort = $HttpsPort + Tls11Port = $Tls11Port + TlsPort = $TlsPort Job = $Job } # Wait until the app is running or until the initTimeoutSeconds have been reached @@ -112,6 +122,10 @@ function Get-WebListenerUrl { [OutputType([Uri])] param ( [switch]$Https, + + [ValidateSet('Default', 'Tls12', 'Tls11', 'Tls')] + [string]$SslProtocol = 'Default', + [ValidateSet( 'Cert', 'Compression', @@ -140,9 +154,16 @@ function Get-WebListenerUrl { $Uri.Host = 'localhost' $Uri.Port = $runningListener.HttpPort $Uri.Scheme = 'Http' + if ($Https.IsPresent) { - $Uri.Port = $runningListener.HttpsPort + switch ($SslProtocol) + { + 'Tls11' { $Uri.Port = $runningListener.Tls11Port } + 'Tls' { $Uri.Port = $runningListener.TlsPort } + # The base HTTPs port is configured for Tls12 only + default { $Uri.Port = $runningListener.HttpsPort } + } $Uri.Scheme = 'Https' } diff --git a/test/tools/WebListener/Program.cs b/test/tools/WebListener/Program.cs index 124f28f7e8d..e0706c9a374 100644 --- a/test/tools/WebListener/Program.cs +++ b/test/tools/WebListener/Program.cs @@ -20,9 +20,9 @@ public class Program { public static void Main(string[] args) { - if (args.Count() != 4) + if (args.Count() != 6) { - System.Console.WriteLine("Required: "); + System.Console.WriteLine("Required: "); Environment.Exit(1); } BuildWebHost(args).Run(); @@ -44,6 +44,28 @@ public static IWebHost BuildWebHost(string[] args) => httpsOption.ServerCertificate = certificate; listenOptions.UseHttps(httpsOption); }); + options.Listen(IPAddress.Loopback, int.Parse(args[4]), listenOptions => + { + var certificate = new X509Certificate2(args[0], args[1]); + HttpsConnectionAdapterOptions httpsOption = new HttpsConnectionAdapterOptions(); + httpsOption.SslProtocols = SslProtocols.Tls11; + httpsOption.ClientCertificateMode = ClientCertificateMode.AllowCertificate; + httpsOption.ClientCertificateValidation = (inCertificate, inChain, inPolicy) => {return true;}; + httpsOption.CheckCertificateRevocation = false; + httpsOption.ServerCertificate = certificate; + listenOptions.UseHttps(httpsOption); + }); + options.Listen(IPAddress.Loopback, int.Parse(args[5]), listenOptions => + { + var certificate = new X509Certificate2(args[0], args[1]); + HttpsConnectionAdapterOptions httpsOption = new HttpsConnectionAdapterOptions(); + httpsOption.SslProtocols = SslProtocols.Tls; + httpsOption.ClientCertificateMode = ClientCertificateMode.AllowCertificate; + httpsOption.ClientCertificateValidation = (inCertificate, inChain, inPolicy) => {return true;}; + httpsOption.CheckCertificateRevocation = false; + httpsOption.ServerCertificate = certificate; + listenOptions.UseHttps(httpsOption); + }); }) .Build(); } diff --git a/test/tools/WebListener/README.md b/test/tools/WebListener/README.md index 04dbd20ccb5..2a26200b826 100644 --- a/test/tools/WebListener/README.md +++ b/test/tools/WebListener/README.md @@ -8,24 +8,26 @@ ASP.NET Core 2.0 app for testing HTTP and HTTPS Requests. dotnet restore dotnet publish --output bin --configuration Release cd bin -dotnet WebListener.dll ServerCert.pfx password 8083 8084 +dotnet WebListener.dll ServerCert.pfx password 8083 8084 8085 8086 ``` -The test site can then be accessed via `http://localhost:8083/` or `https://localhost:8084/`. +The test site can then be accessed via `http://localhost:8083/`, `https://localhost:8084/`, `https://localhost:8085/`, or `https://localhost:8086/`. -The `WebListener.dll` takes 4 arguments: +The `WebListener.dll` takes 6 arguments: * The path to the Server Certificate * The Server Certificate Password * The TCP Port to bind on for HTTP -* The TCP Port to bind on for HTTPS +* The TCP Port to bind on for HTTPS using TLS 1.2 +* The TCP Port to bind on for HTTPS using TLS 1.1 +* The TCP Port to bind on for HTTPS using TLS 1.0 # Run With WebListener Module ```powershell Import-Module .\build.psm1 Publish-PSTestTools -$Listener = Start-WebListener -HttpPort 8083 -HttpsPort 8084 +$Listener = Start-WebListener -HttpPort 8083 -HttpsPort 8084 -Tls11Port 8085 -TlsPort = 8086 ``` # Tests