From d653ab07638c972cc7747c96448ff5576e3a779e Mon Sep 17 00:00:00 2001 From: David Negstad Date: Tue, 6 Jan 2026 18:09:04 -0800 Subject: [PATCH 1/8] Add ability to trust certificate in windows store from WSL --- .../CertificateManager.cs | 9 +++ .../UnixCertificateManager.cs | 68 +++++++++++++++++++ 2 files changed, 77 insertions(+) diff --git a/src/Shared/CertificateGeneration/CertificateManager.cs b/src/Shared/CertificateGeneration/CertificateManager.cs index 6d4f847158f3..1396c0aeed70 100644 --- a/src/Shared/CertificateGeneration/CertificateManager.cs +++ b/src/Shared/CertificateGeneration/CertificateManager.cs @@ -1313,6 +1313,15 @@ public sealed class CertificateManagerEventSource : EventSource [Event(112, Level = EventLevel.Warning, Message = "Directory '{0}' may be readable by other users.")] internal void DirectoryPermissionsNotSecure(string directoryPath) => WriteEvent(112, directoryPath); + + [Event(113, Level = EventLevel.Verbose, Message = "Successfully trusted the certificate in the Windows certificate store via WSL.")] + internal void WslWindowsTrustSucceeded() => WriteEvent(113); + + [Event(114, Level = EventLevel.Warning, Message = "Failed to trust the certificate in the Windows certificate store via WSL.")] + internal void WslWindowsTrustFailed() => WriteEvent(114); + + [Event(115, Level = EventLevel.Warning, Message = "Failed to trust the certificate in the Windows certificate store via WSL: {0}.")] + internal void WslWindowsTrustException(string exceptionMessage) => WriteEvent(115, exceptionMessage); } internal sealed class UserCancelledTrustException : Exception diff --git a/src/Shared/CertificateGeneration/UnixCertificateManager.cs b/src/Shared/CertificateGeneration/UnixCertificateManager.cs index 10cfcc08d38f..fa9aa6f64d2e 100644 --- a/src/Shared/CertificateGeneration/UnixCertificateManager.cs +++ b/src/Shared/CertificateGeneration/UnixCertificateManager.cs @@ -32,6 +32,10 @@ internal sealed partial class UnixCertificateManager : CertificateManager private const string BrowserFamilyChromium = "Chromium"; private const string BrowserFamilyFirefox = "Firefox"; + private const string PowerShellCommand = "powershell.exe"; + private const string WslInteropPath = "/proc/sys/fs/binfmt_misc/WSLInterop"; + private const string WslInteropLatePath = "/proc/sys/fs/binfmt_misc/WSLInterop-late"; + private const string OpenSslCommand = "openssl"; private const string CertUtilCommand = "certutil"; @@ -365,6 +369,21 @@ protected override TrustLevel TrustCertificateCore(X509Certificate2 certificate) } } + // Check to see if we're running in WSL; if so, use powershell.exe to add the certificate to the Windows trust store as well + if (IsRunningOnWsl()) + { + if (TryTrustCertificateInWindowsStore(certPath)) + { + Log.WslWindowsTrustSucceeded(); + sawTrustSuccess = true; + } + else + { + Log.WslWindowsTrustFailed(); + sawTrustFailure = true; + } + } + return sawTrustFailure ? TrustLevel.Partial : TrustLevel.Full; @@ -564,6 +583,55 @@ private static string GetCertificateNickname(X509Certificate2 certificate) return $"aspnetcore-localhost-{certificate.Thumbprint}"; } + /// + /// Detects if the current environment is Windows Subsystem for Linux (WSL). + /// + /// True if running on WSL; otherwise, false. + private static bool IsRunningOnWsl() + { + // WSL exposes special files that indicate WSL interop is enabled. + // Either WSLInterop or WSLInterop-late may be present depending on the WSL version and configuration. + return File.Exists(WslInteropPath) || File.Exists(WslInteropLatePath); + } + + /// + /// Attempts to trust the certificate in the Windows certificate store via PowerShell when running on WSL. + /// + /// The path to the certificate file (PEM format). + /// True if the certificate was successfully added to the Windows store; otherwise, false. + private static bool TryTrustCertificateInWindowsStore(string certificatePath) + { + // PowerShell command to import the certificate into the CurrentUser Root store. + // We use Import-Certificate which can handle PEM files on modern Windows. + // The -CertStoreLocation parameter specifies the store location. + var escapedPath = certificatePath.Replace("'", "''"); + var powershellScript = $@" + $cert = New-Object System.Security.Cryptography.X509Certificates.X509Certificate2('{escapedPath}') + $store = New-Object System.Security.Cryptography.X509Certificates.X509Store('Root', 'CurrentUser') + $store.Open('ReadWrite') + $store.Add($cert) + $store.Close() + "; + + var startInfo = new ProcessStartInfo(PowerShellCommand, $"-NoProfile -NonInteractive -Command \"{powershellScript}\"") + { + RedirectStandardOutput = true, + RedirectStandardError = true, + }; + + try + { + using var process = Process.Start(startInfo)!; + process.WaitForExit(); + return process.ExitCode == 0; + } + catch (Exception ex) + { + Log.WslWindowsTrustException(ex.Message); + return false; + } + } + /// /// It is the caller's responsibility to ensure that is available. /// From 6efd7e67d8f300bbcd9133ca7746d2ccc9efaa9a Mon Sep 17 00:00:00 2001 From: David Negstad Date: Wed, 7 Jan 2026 10:11:40 -0800 Subject: [PATCH 2/8] Add certificate with friendly name and additional WSL checks --- .../UnixCertificateManager.cs | 25 +++++++++++++++---- 1 file changed, 20 insertions(+), 5 deletions(-) diff --git a/src/Shared/CertificateGeneration/UnixCertificateManager.cs b/src/Shared/CertificateGeneration/UnixCertificateManager.cs index fa9aa6f64d2e..059a9e04e14d 100644 --- a/src/Shared/CertificateGeneration/UnixCertificateManager.cs +++ b/src/Shared/CertificateGeneration/UnixCertificateManager.cs @@ -35,6 +35,7 @@ internal sealed partial class UnixCertificateManager : CertificateManager private const string PowerShellCommand = "powershell.exe"; private const string WslInteropPath = "/proc/sys/fs/binfmt_misc/WSLInterop"; private const string WslInteropLatePath = "/proc/sys/fs/binfmt_misc/WSLInterop-late"; + private const string WslFriendlyName = AspNetHttpsOidFriendlyName + " (WSL)"; private const string OpenSslCommand = "openssl"; private const string CertUtilCommand = "certutil"; @@ -370,7 +371,7 @@ protected override TrustLevel TrustCertificateCore(X509Certificate2 certificate) } // Check to see if we're running in WSL; if so, use powershell.exe to add the certificate to the Windows trust store as well - if (IsRunningOnWsl()) + if (IsRunningOnWslWithInterop()) { if (TryTrustCertificateInWindowsStore(certPath)) { @@ -584,14 +585,26 @@ private static string GetCertificateNickname(X509Certificate2 certificate) } /// - /// Detects if the current environment is Windows Subsystem for Linux (WSL). + /// Detects if the current environment is Windows Subsystem for Linux (WSL) with interop enabled. /// - /// True if running on WSL; otherwise, false. - private static bool IsRunningOnWsl() + /// True if running on WSL with interop; otherwise, false. + private static bool IsRunningOnWslWithInterop() { // WSL exposes special files that indicate WSL interop is enabled. // Either WSLInterop or WSLInterop-late may be present depending on the WSL version and configuration. - return File.Exists(WslInteropPath) || File.Exists(WslInteropLatePath); + if (File.Exists(WslInteropPath) || File.Exists(WslInteropLatePath)) + { + return true; + } + + // Additionally check for standard WSL environment variables as a fallback. + // WSL_INTEROP is set to the path of the interop socket. + if (!string.IsNullOrEmpty(Environment.GetEnvironmentVariable("WSL_INTEROP"))) + { + return true; + } + + return false; } /// @@ -605,8 +618,10 @@ private static bool TryTrustCertificateInWindowsStore(string certificatePath) // We use Import-Certificate which can handle PEM files on modern Windows. // The -CertStoreLocation parameter specifies the store location. var escapedPath = certificatePath.Replace("'", "''"); + var escapedFriendlyName = WslFriendlyName.Replace("'", "''"); var powershellScript = $@" $cert = New-Object System.Security.Cryptography.X509Certificates.X509Certificate2('{escapedPath}') + $cert.FriendlyName = '{escapedFriendlyName}' $store = New-Object System.Security.Cryptography.X509Certificates.X509Store('Root', 'CurrentUser') $store.Open('ReadWrite') $store.Add($cert) From 89676e0ced9f24f8af17e0939b9bfa9c2b32bfd0 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 7 Jan 2026 20:08:46 +0000 Subject: [PATCH 3/8] Initial plan From e31abab7251550149b5681feb0019f0def3e7617 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 7 Jan 2026 20:14:44 +0000 Subject: [PATCH 4/8] Fix command injection vulnerability using -EncodedCommand with Base64 Co-authored-by: danegsta <50252651+danegsta@users.noreply.github.com> --- src/Shared/CertificateGeneration/UnixCertificateManager.cs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/Shared/CertificateGeneration/UnixCertificateManager.cs b/src/Shared/CertificateGeneration/UnixCertificateManager.cs index 059a9e04e14d..3ade27b2f3af 100644 --- a/src/Shared/CertificateGeneration/UnixCertificateManager.cs +++ b/src/Shared/CertificateGeneration/UnixCertificateManager.cs @@ -617,6 +617,8 @@ private static bool TryTrustCertificateInWindowsStore(string certificatePath) // PowerShell command to import the certificate into the CurrentUser Root store. // We use Import-Certificate which can handle PEM files on modern Windows. // The -CertStoreLocation parameter specifies the store location. + // Using -EncodedCommand with Base64 encoding to avoid all shell escaping issues, + // particularly command injection vulnerabilities from paths with special characters. var escapedPath = certificatePath.Replace("'", "''"); var escapedFriendlyName = WslFriendlyName.Replace("'", "''"); var powershellScript = $@" @@ -628,7 +630,10 @@ private static bool TryTrustCertificateInWindowsStore(string certificatePath) $store.Close() "; - var startInfo = new ProcessStartInfo(PowerShellCommand, $"-NoProfile -NonInteractive -Command \"{powershellScript}\"") + // Encode the PowerShell script to Base64 (UTF-16LE as required by PowerShell) + var encodedCommand = Convert.ToBase64String(System.Text.Encoding.Unicode.GetBytes(powershellScript)); + + var startInfo = new ProcessStartInfo(PowerShellCommand, $"-NoProfile -NonInteractive -EncodedCommand {encodedCommand}") { RedirectStandardOutput = true, RedirectStandardError = true, From 497b40fcb0444a87e72fde26b22d72122b0ddc64 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 7 Jan 2026 20:16:45 +0000 Subject: [PATCH 5/8] Remove redundant string escaping since Base64 encoding handles all special characters Co-authored-by: danegsta <50252651+danegsta@users.noreply.github.com> --- src/Shared/CertificateGeneration/UnixCertificateManager.cs | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/Shared/CertificateGeneration/UnixCertificateManager.cs b/src/Shared/CertificateGeneration/UnixCertificateManager.cs index 3ade27b2f3af..d1545fc17fa3 100644 --- a/src/Shared/CertificateGeneration/UnixCertificateManager.cs +++ b/src/Shared/CertificateGeneration/UnixCertificateManager.cs @@ -619,11 +619,9 @@ private static bool TryTrustCertificateInWindowsStore(string certificatePath) // The -CertStoreLocation parameter specifies the store location. // Using -EncodedCommand with Base64 encoding to avoid all shell escaping issues, // particularly command injection vulnerabilities from paths with special characters. - var escapedPath = certificatePath.Replace("'", "''"); - var escapedFriendlyName = WslFriendlyName.Replace("'", "''"); var powershellScript = $@" - $cert = New-Object System.Security.Cryptography.X509Certificates.X509Certificate2('{escapedPath}') - $cert.FriendlyName = '{escapedFriendlyName}' + $cert = New-Object System.Security.Cryptography.X509Certificates.X509Certificate2('{certificatePath}') + $cert.FriendlyName = '{WslFriendlyName}' $store = New-Object System.Security.Cryptography.X509Certificates.X509Store('Root', 'CurrentUser') $store.Open('ReadWrite') $store.Add($cert) From bcbf3ac26496a21dd916d3be5ee738ffdf6c2689 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 7 Jan 2026 20:18:36 +0000 Subject: [PATCH 6/8] Re-add PowerShell single-quote escaping to prevent injection within PS script Co-authored-by: danegsta <50252651+danegsta@users.noreply.github.com> --- .../CertificateGeneration/UnixCertificateManager.cs | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/Shared/CertificateGeneration/UnixCertificateManager.cs b/src/Shared/CertificateGeneration/UnixCertificateManager.cs index d1545fc17fa3..664c6a24ab69 100644 --- a/src/Shared/CertificateGeneration/UnixCertificateManager.cs +++ b/src/Shared/CertificateGeneration/UnixCertificateManager.cs @@ -617,11 +617,14 @@ private static bool TryTrustCertificateInWindowsStore(string certificatePath) // PowerShell command to import the certificate into the CurrentUser Root store. // We use Import-Certificate which can handle PEM files on modern Windows. // The -CertStoreLocation parameter specifies the store location. - // Using -EncodedCommand with Base64 encoding to avoid all shell escaping issues, - // particularly command injection vulnerabilities from paths with special characters. + // Using -EncodedCommand with Base64 encoding to avoid command shell escaping issues. + // We still need to escape single quotes within the PowerShell script itself to prevent + // PowerShell injection vulnerabilities. + var escapedPath = certificatePath.Replace("'", "''"); + var escapedFriendlyName = WslFriendlyName.Replace("'", "''"); var powershellScript = $@" - $cert = New-Object System.Security.Cryptography.X509Certificates.X509Certificate2('{certificatePath}') - $cert.FriendlyName = '{WslFriendlyName}' + $cert = New-Object System.Security.Cryptography.X509Certificates.X509Certificate2('{escapedPath}') + $cert.FriendlyName = '{escapedFriendlyName}' $store = New-Object System.Security.Cryptography.X509Certificates.X509Store('Root', 'CurrentUser') $store.Open('ReadWrite') $store.Add($cert) From ac96f889a0e8b0a681157fbc9f023e9a000dc5f8 Mon Sep 17 00:00:00 2001 From: David Negstad Date: Wed, 7 Jan 2026 14:12:07 -0800 Subject: [PATCH 7/8] Read the certificate from bytes instead of from a file path --- .../UnixCertificateManager.cs | 58 +++++++++---------- 1 file changed, 29 insertions(+), 29 deletions(-) diff --git a/src/Shared/CertificateGeneration/UnixCertificateManager.cs b/src/Shared/CertificateGeneration/UnixCertificateManager.cs index 664c6a24ab69..65034bbd8c44 100644 --- a/src/Shared/CertificateGeneration/UnixCertificateManager.cs +++ b/src/Shared/CertificateGeneration/UnixCertificateManager.cs @@ -373,7 +373,7 @@ protected override TrustLevel TrustCertificateCore(X509Certificate2 certificate) // Check to see if we're running in WSL; if so, use powershell.exe to add the certificate to the Windows trust store as well if (IsRunningOnWslWithInterop()) { - if (TryTrustCertificateInWindowsStore(certPath)) + if (TryTrustCertificateInWindowsStore(certificate)) { Log.WslWindowsTrustSucceeded(); sawTrustSuccess = true; @@ -610,38 +610,38 @@ private static bool IsRunningOnWslWithInterop() /// /// Attempts to trust the certificate in the Windows certificate store via PowerShell when running on WSL. /// - /// The path to the certificate file (PEM format). + /// The certificate to trust. /// True if the certificate was successfully added to the Windows store; otherwise, false. - private static bool TryTrustCertificateInWindowsStore(string certificatePath) + private static bool TryTrustCertificateInWindowsStore(X509Certificate2 certificate) { - // PowerShell command to import the certificate into the CurrentUser Root store. - // We use Import-Certificate which can handle PEM files on modern Windows. - // The -CertStoreLocation parameter specifies the store location. - // Using -EncodedCommand with Base64 encoding to avoid command shell escaping issues. - // We still need to escape single quotes within the PowerShell script itself to prevent - // PowerShell injection vulnerabilities. - var escapedPath = certificatePath.Replace("'", "''"); - var escapedFriendlyName = WslFriendlyName.Replace("'", "''"); - var powershellScript = $@" - $cert = New-Object System.Security.Cryptography.X509Certificates.X509Certificate2('{escapedPath}') - $cert.FriendlyName = '{escapedFriendlyName}' - $store = New-Object System.Security.Cryptography.X509Certificates.X509Store('Root', 'CurrentUser') - $store.Open('ReadWrite') - $store.Add($cert) - $store.Close() - "; - - // Encode the PowerShell script to Base64 (UTF-16LE as required by PowerShell) - var encodedCommand = Convert.ToBase64String(System.Text.Encoding.Unicode.GetBytes(powershellScript)); - - var startInfo = new ProcessStartInfo(PowerShellCommand, $"-NoProfile -NonInteractive -EncodedCommand {encodedCommand}") - { - RedirectStandardOutput = true, - RedirectStandardError = true, - }; - try { + // Export the certificate as DER-encoded bytes (no private key needed for trust) + // and embed it directly in the PowerShell script as Base64 to avoid file path + // translation issues between WSL and Windows. + var certBytes = certificate.Export(X509ContentType.Cert); + var certBase64 = Convert.ToBase64String(certBytes); + + var escapedFriendlyName = WslFriendlyName.Replace("'", "''"); + var powershellScript = $@" + $certBytes = [Convert]::FromBase64String('{certBase64}') + $cert = New-Object System.Security.Cryptography.X509Certificates.X509Certificate2(,$certBytes) + $cert.FriendlyName = '{escapedFriendlyName}' + $store = New-Object System.Security.Cryptography.X509Certificates.X509Store('Root', 'CurrentUser') + $store.Open('ReadWrite') + $store.Add($cert) + $store.Close() + "; + + // Encode the PowerShell script to Base64 (UTF-16LE as required by PowerShell) + var encodedCommand = Convert.ToBase64String(System.Text.Encoding.Unicode.GetBytes(powershellScript)); + + var startInfo = new ProcessStartInfo(PowerShellCommand, $"-NoProfile -NonInteractive -EncodedCommand {encodedCommand}") + { + RedirectStandardOutput = true, + RedirectStandardError = true, + }; + using var process = Process.Start(startInfo)!; process.WaitForExit(); return process.ExitCode == 0; From 1579e80692ed5659492343d85c01597ac8182bb6 Mon Sep 17 00:00:00 2001 From: David Negstad Date: Thu, 8 Jan 2026 13:01:25 -0800 Subject: [PATCH 8/8] Add a comment pointing out that the cert trust is a no-op if the cert already exists and ensure that only a single error message is shown if there's an exception --- .../UnixCertificateManager.cs | 82 +++++++++---------- 1 file changed, 41 insertions(+), 41 deletions(-) diff --git a/src/Shared/CertificateGeneration/UnixCertificateManager.cs b/src/Shared/CertificateGeneration/UnixCertificateManager.cs index 65034bbd8c44..801fa4081ec8 100644 --- a/src/Shared/CertificateGeneration/UnixCertificateManager.cs +++ b/src/Shared/CertificateGeneration/UnixCertificateManager.cs @@ -373,14 +373,21 @@ protected override TrustLevel TrustCertificateCore(X509Certificate2 certificate) // Check to see if we're running in WSL; if so, use powershell.exe to add the certificate to the Windows trust store as well if (IsRunningOnWslWithInterop()) { - if (TryTrustCertificateInWindowsStore(certificate)) + try { - Log.WslWindowsTrustSucceeded(); - sawTrustSuccess = true; + if (TrustCertificateInWindowsStore(certificate)) + { + Log.WslWindowsTrustSucceeded(); + } + else + { + Log.WslWindowsTrustFailed(); + sawTrustFailure = true; + } } - else + catch (Exception ex) { - Log.WslWindowsTrustFailed(); + Log.WslWindowsTrustException(ex.Message); sawTrustFailure = true; } } @@ -609,48 +616,41 @@ private static bool IsRunningOnWslWithInterop() /// /// Attempts to trust the certificate in the Windows certificate store via PowerShell when running on WSL. + /// If the certificate already exists in the store, this is a no-op. /// /// The certificate to trust. /// True if the certificate was successfully added to the Windows store; otherwise, false. - private static bool TryTrustCertificateInWindowsStore(X509Certificate2 certificate) + private static bool TrustCertificateInWindowsStore(X509Certificate2 certificate) { - try + // Export the certificate as DER-encoded bytes (no private key needed for trust) + // and embed it directly in the PowerShell script as Base64 to avoid file path + // translation issues between WSL and Windows. + var certBytes = certificate.Export(X509ContentType.Cert); + var certBase64 = Convert.ToBase64String(certBytes); + + var escapedFriendlyName = WslFriendlyName.Replace("'", "''"); + var powershellScript = $@" + $certBytes = [Convert]::FromBase64String('{certBase64}') + $cert = New-Object System.Security.Cryptography.X509Certificates.X509Certificate2(,$certBytes) + $cert.FriendlyName = '{escapedFriendlyName}' + $store = New-Object System.Security.Cryptography.X509Certificates.X509Store('Root', 'CurrentUser') + $store.Open('ReadWrite') + $store.Add($cert) + $store.Close() + "; + + // Encode the PowerShell script to Base64 (UTF-16LE as required by PowerShell) + var encodedCommand = Convert.ToBase64String(System.Text.Encoding.Unicode.GetBytes(powershellScript)); + + var startInfo = new ProcessStartInfo(PowerShellCommand, $"-NoProfile -NonInteractive -EncodedCommand {encodedCommand}") { - // Export the certificate as DER-encoded bytes (no private key needed for trust) - // and embed it directly in the PowerShell script as Base64 to avoid file path - // translation issues between WSL and Windows. - var certBytes = certificate.Export(X509ContentType.Cert); - var certBase64 = Convert.ToBase64String(certBytes); - - var escapedFriendlyName = WslFriendlyName.Replace("'", "''"); - var powershellScript = $@" - $certBytes = [Convert]::FromBase64String('{certBase64}') - $cert = New-Object System.Security.Cryptography.X509Certificates.X509Certificate2(,$certBytes) - $cert.FriendlyName = '{escapedFriendlyName}' - $store = New-Object System.Security.Cryptography.X509Certificates.X509Store('Root', 'CurrentUser') - $store.Open('ReadWrite') - $store.Add($cert) - $store.Close() - "; - - // Encode the PowerShell script to Base64 (UTF-16LE as required by PowerShell) - var encodedCommand = Convert.ToBase64String(System.Text.Encoding.Unicode.GetBytes(powershellScript)); - - var startInfo = new ProcessStartInfo(PowerShellCommand, $"-NoProfile -NonInteractive -EncodedCommand {encodedCommand}") - { - RedirectStandardOutput = true, - RedirectStandardError = true, - }; + RedirectStandardOutput = true, + RedirectStandardError = true, + }; - using var process = Process.Start(startInfo)!; - process.WaitForExit(); - return process.ExitCode == 0; - } - catch (Exception ex) - { - Log.WslWindowsTrustException(ex.Message); - return false; - } + using var process = Process.Start(startInfo)!; + process.WaitForExit(); + return process.ExitCode == 0; } ///