Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions src/Shared/CertificateGeneration/CertificateManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
89 changes: 89 additions & 0 deletions src/Shared/CertificateGeneration/UnixCertificateManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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";

Expand Down Expand Up @@ -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())
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we care if the other linux checks run? Should this check if sawTrustFailure is already true?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think it makes sense to skip just because we saw a failure as it still makes sense to run in a partial success state. There's already a check on line 338 that will exit early if there wasn't any success trusting the certificate, so if we made it to this point the certificate was at least trusted in some capacity. We still want all the other Linux specific logic to run on WSL since the goal is to trust it in both WSL and Windows in that case.

{
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;
Expand Down Expand Up @@ -607,6 +634,68 @@ private static string GetCertificateNickname(X509Certificate2 certificate)
return $"aspnetcore-localhost-{certificate.Thumbprint}";
}

/// <summary>
/// Detects if the current environment is Windows Subsystem for Linux (WSL) with interop enabled.
/// </summary>
/// <returns>True if running on WSL with interop; otherwise, false.</returns>
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;
}

/// <summary>
/// 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.
/// </summary>
/// <param name="certificate">The certificate to trust.</param>
/// <returns>True if the certificate was successfully added to the Windows store; otherwise, false.</returns>
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;
}

/// <remarks>
/// It is the caller's responsibility to ensure that <see cref="CertUtilCommand"/> is available.
/// </remarks>
Expand Down
Loading