Skip to content
Merged
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(113);

[Event(116, Level = EventLevel.Warning, Message = "Failed to trust the certificate in the Windows certificate store via WSL.")]
internal void WslWindowsTrustFailed() => WriteEvent(114);

[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(115, 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())
{
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