Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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 @@ -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
Expand Down
83 changes: 83 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 @@ -365,6 +370,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())
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.

{
if (TryTrustCertificateInWindowsStore(certPath))
{
Log.WslWindowsTrustSucceeded();
sawTrustSuccess = true;
}
else
{
Log.WslWindowsTrustFailed();
sawTrustFailure = true;
}
}

return sawTrustFailure
? TrustLevel.Partial
: TrustLevel.Full;
Expand Down Expand Up @@ -564,6 +584,69 @@ 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.
/// </summary>
/// <param name="certificatePath">The path to the certificate file (PEM format).</param>
/// <returns>True if the certificate was successfully added to the Windows store; otherwise, false.</returns>
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 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()
";

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;
}
}

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