diff --git a/src/Shared/CertificateGeneration/CertificateManager.cs b/src/Shared/CertificateGeneration/CertificateManager.cs index 5430a956d7c7..9190317acc93 100644 --- a/src/Shared/CertificateGeneration/CertificateManager.cs +++ b/src/Shared/CertificateGeneration/CertificateManager.cs @@ -1318,6 +1318,15 @@ public sealed class CertificateManagerEventSource : EventSource "For example, `export {1}=\"{0}:${1}\"`. " + "See https://aka.ms/dev-certs-trust for more information.")] internal void UnixSuggestAppendingToEnvironmentVariable(string certDir, string envVarName) => WriteEvent(114, certDir, envVarName); + + [Event(115, Level = EventLevel.Verbose, Message = "Successfully trusted the certificate in the Windows certificate store via WSL.")] + internal void WslWindowsTrustSucceeded() => WriteEvent(115); + + [Event(116, Level = EventLevel.Warning, Message = "Failed to trust the certificate in the Windows certificate store via WSL.")] + internal void WslWindowsTrustFailed() => WriteEvent(116); + + [Event(117, Level = EventLevel.Warning, Message = "Failed to trust the certificate in the Windows certificate store via WSL: {0}.")] + internal void WslWindowsTrustException(string exceptionMessage) => WriteEvent(117, exceptionMessage); } internal sealed class UserCancelledTrustException : Exception diff --git a/src/Shared/CertificateGeneration/UnixCertificateManager.cs b/src/Shared/CertificateGeneration/UnixCertificateManager.cs index 6928dffd0239..e9a3a953a91c 100644 --- a/src/Shared/CertificateGeneration/UnixCertificateManager.cs +++ b/src/Shared/CertificateGeneration/UnixCertificateManager.cs @@ -32,6 +32,11 @@ 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 WslFriendlyName = AspNetHttpsOidFriendlyName + " (WSL)"; + private const string OpenSslCommand = "openssl"; private const string CertUtilCommand = "certutil"; @@ -408,6 +413,28 @@ protected override TrustLevel TrustCertificateCore(X509Certificate2 certificate) sawTrustFailure = !hasValidSslCertDir; } + // 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()) + { + try + { + if (TrustCertificateInWindowsStore(certificate)) + { + Log.WslWindowsTrustSucceeded(); + } + else + { + Log.WslWindowsTrustFailed(); + sawTrustFailure = true; + } + } + catch (Exception ex) + { + Log.WslWindowsTrustException(ex.Message); + sawTrustFailure = true; + } + } + return sawTrustFailure ? TrustLevel.Partial : TrustLevel.Full; @@ -607,6 +634,68 @@ private static string GetCertificateNickname(X509Certificate2 certificate) return $"aspnetcore-localhost-{certificate.Thumbprint}"; } + /// + /// Detects if the current environment is Windows Subsystem for Linux (WSL) with interop enabled. + /// + /// 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. + 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; + } + + /// + /// 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 TrustCertificateInWindowsStore(X509Certificate2 certificate) + { + // 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; + } + /// /// It is the caller's responsibility to ensure that is available. ///