Skip to content
Open
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
13 changes: 9 additions & 4 deletions src/Shared/CertificateGeneration/CertificateManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -966,9 +966,6 @@ internal static bool TryFindCertificateInStore(X509Store store, X509Certificate2
return foundCertificate is not null;
}

/// <remarks>
/// Note that dotnet-dev-certs won't display any of these, regardless of level, unless --verbose is passed.
/// </remarks>
[EventSource(Name = "Dotnet-dev-certs")]
public sealed class CertificateManagerEventSource : EventSource
{
Expand Down Expand Up @@ -1303,7 +1300,7 @@ public sealed class CertificateManagerEventSource : EventSource
internal void UnixNotOverwritingCertificate(string certPath) => WriteEvent(109, certPath);

[Event(110, Level = EventLevel.LogAlways, Message = "For OpenSSL trust to take effect, '{0}' must be listed in the {2} environment variable. " +
"For example, `export SSL_CERT_DIR={0}:{1}`. " +
"For example, `export {2}=\"{0}:{1}\"`. " +
"See https://aka.ms/dev-certs-trust for more information.")]
internal void UnixSuggestSettingEnvironmentVariable(string certDir, string openSslDir, string envVarName) => WriteEvent(110, certDir, openSslDir, envVarName);

Expand All @@ -1313,6 +1310,14 @@ 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 = "The certificate directory '{0}' is already included in the {1} environment variable.")]
internal void UnixOpenSslCertificateDirectoryAlreadyConfigured(string certDir, string envVarName) => WriteEvent(113, certDir, envVarName);

[Event(114, Level = EventLevel.LogAlways, Message = "For OpenSSL trust to take effect, '{0}' must be listed in the {1} environment variable. " +
"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);
}

internal sealed class UserCancelledTrustException : Exception
Expand Down
60 changes: 56 additions & 4 deletions src/Shared/CertificateGeneration/UnixCertificateManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -355,14 +355,57 @@ protected override TrustLevel TrustCertificateCore(X509Certificate2 certificate)
? Path.Combine("$HOME", certDir[homeDirectoryWithSlash.Length..])
: certDir;

if (TryGetOpenSslDirectory(out var openSslDir))
var hasValidSslCertDir = false;

// Check if SSL_CERT_DIR is already set and if certDir is already included
var existingSslCertDir = Environment.GetEnvironmentVariable(OpenSslCertificateDirectoryVariableName);
if (!string.IsNullOrEmpty(existingSslCertDir))
{
var existingDirs = existingSslCertDir.Split(Path.PathSeparator);
var certDirFullPath = Path.GetFullPath(prettyCertDir);
var isCertDirIncluded = existingDirs.Any(dir =>
{
if (string.IsNullOrWhiteSpace(dir))
{
return false;
}

try
{
return string.Equals(Path.GetFullPath(dir), certDirFullPath, StringComparison.OrdinalIgnoreCase);
}
catch
{
// Ignore invalid directory entries in SSL_CERT_DIR
return false;
}
});
Comment on lines +362 to +382
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Bug: Path comparison uses display string with unexpanded $HOME placeholder.

prettyCertDir may contain the literal string "$HOME" (set at lines 354-356 for display purposes). Path.GetFullPath() does not expand shell variables—it treats $HOME as a literal directory component, producing an incorrect path like /cwd/$HOME/.aspnet/dev-certs/trust rather than /home/user/.aspnet/dev-certs/trust.

This causes the comparison against SSL_CERT_DIR entries to always fail when it should succeed.

🐛 Proposed fix: use the actual path for comparison
             var existingSslCertDir = Environment.GetEnvironmentVariable(OpenSslCertificateDirectoryVariableName);
             if (!string.IsNullOrEmpty(existingSslCertDir))
             {
                 var existingDirs = existingSslCertDir.Split(Path.PathSeparator);
-                var certDirFullPath = Path.GetFullPath(prettyCertDir);
+                var certDirFullPath = Path.GetFullPath(certDir);
                 var isCertDirIncluded = existingDirs.Any(dir =>
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (!string.IsNullOrEmpty(existingSslCertDir))
{
var existingDirs = existingSslCertDir.Split(Path.PathSeparator);
var certDirFullPath = Path.GetFullPath(prettyCertDir);
var isCertDirIncluded = existingDirs.Any(dir =>
{
if (string.IsNullOrWhiteSpace(dir))
{
return false;
}
try
{
return string.Equals(Path.GetFullPath(dir), certDirFullPath, StringComparison.OrdinalIgnoreCase);
}
catch
{
// Ignore invalid directory entries in SSL_CERT_DIR
return false;
}
});
if (!string.IsNullOrEmpty(existingSslCertDir))
{
var existingDirs = existingSslCertDir.Split(Path.PathSeparator);
var certDirFullPath = Path.GetFullPath(certDir);
var isCertDirIncluded = existingDirs.Any(dir =>
{
if (string.IsNullOrWhiteSpace(dir))
{
return false;
}
try
{
return string.Equals(Path.GetFullPath(dir), certDirFullPath, StringComparison.OrdinalIgnoreCase);
}
catch
{
// Ignore invalid directory entries in SSL_CERT_DIR
return false;
}
});
🤖 Prompt for AI Agents
In `@src/Shared/CertificateGeneration/UnixCertificateManager.cs` around lines 362
- 382, The path comparison is using the display variable prettyCertDir (which
may contain the literal "$HOME") so Path.GetFullPath(...) yields a wrong string;
change the code in the SSL_CERT_DIR membership check to use the real/expanded
certificate directory variable (e.g., certDir or whatever holds the actual
expanded path) instead of prettyCertDir — compute certDirFullPath =
Path.GetFullPath(certDir) and use that in the Any(...) comparison and
string.Equals call, leaving the existing try/catch and whitespace checks intact.


if (isCertDirIncluded)
{
// The certificate directory is already in SSL_CERT_DIR, no action needed
Log.UnixOpenSslCertificateDirectoryAlreadyConfigured(prettyCertDir, OpenSslCertificateDirectoryVariableName);
hasValidSslCertDir = true;
}
else
{
// SSL_CERT_DIR is set but doesn't include our directory - suggest appending
Log.UnixSuggestAppendingToEnvironmentVariable(prettyCertDir, OpenSslCertificateDirectoryVariableName);
hasValidSslCertDir = false;
}
}
else if (TryGetOpenSslDirectory(out var openSslDir))
{
Log.UnixSuggestSettingEnvironmentVariable(prettyCertDir, Path.Combine(openSslDir, "certs"), OpenSslCertificateDirectoryVariableName);
hasValidSslCertDir = false;
}
else
{
Log.UnixSuggestSettingEnvironmentVariableWithoutExample(prettyCertDir, OpenSslCertificateDirectoryVariableName);
hasValidSslCertDir = false;
}

sawTrustFailure = !hasValidSslCertDir;
}

return sawTrustFailure
Expand Down Expand Up @@ -948,9 +991,18 @@ private static bool TryRehashOpenSslCertificates(string certificateDirectory)
return true;
}

private sealed class NssDb(string path, bool isFirefox)
private sealed class NssDb
{
public string Path => path;
public bool IsFirefox => isFirefox;
private readonly string _path;
private readonly bool _isFirefox;

public NssDb(string path, bool isFirefox)
{
_path = path;
_isFirefox = isFirefox;
}

public string Path => _path;
public bool IsFirefox => _isFirefox;
}
}
8 changes: 6 additions & 2 deletions src/Tools/dotnet-dev-certs/src/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -124,16 +124,20 @@ public static int Main(string[] args)
{
var reporter = new ConsoleReporter(PhysicalConsole.Singleton, verbose.HasValue(), quiet.HasValue());

var listener = new ReporterEventListener(reporter);
if (verbose.HasValue())
{
var listener = new ReporterEventListener(reporter);
listener.EnableEvents(CertificateManager.Log, System.Diagnostics.Tracing.EventLevel.Verbose);
}
else
{
listener.EnableEvents(CertificateManager.Log, System.Diagnostics.Tracing.EventLevel.LogAlways);
}

if (checkJsonOutput.HasValue())
{
if (exportPath.HasValue() || trust?.HasValue() == true || format.HasValue() || noPassword.HasValue() || check.HasValue() || clean.HasValue() ||
(!import.HasValue() && password.HasValue()) ||
(!import.HasValue() && password.HasValue()) ||
(import.HasValue() && !password.HasValue()))
{
reporter.Error(InvalidUsageErrorMessage);
Expand Down