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.
///