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);
Copy link

Copilot AI Jan 25, 2026

Choose a reason for hiding this comment

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

Using Path.GetFullPath(prettyCertDir) is incorrect because prettyCertDir may contain $HOME (a shell variable placeholder for display purposes), not an actual path. When certDir starts with the home directory, line 355 replaces it with $HOME, making prettyCertDir a display string like $HOME/.aspnet/dev-certs/trust. Calling Path.GetFullPath() on this will not work correctly as it doesn't expand shell variables.

Instead, use certDir (which is the actual full path) for comparison. The variable prettyCertDir should only be used for display in log messages.

Suggested change
var certDirFullPath = Path.GetFullPath(prettyCertDir);
var certDirFullPath = Path.GetFullPath(certDir);

Copilot uses AI. Check for mistakes.
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 (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;
Copy link

Copilot AI Jan 25, 2026

Choose a reason for hiding this comment

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

The assignment sawTrustFailure = !hasValidSslCertDir; unconditionally overwrites any previous value of sawTrustFailure. This is problematic because if there was already a trust failure from earlier checks (e.g., NSS database trust failed at lines 322 or 328), but SSL_CERT_DIR is properly configured (hasValidSslCertDir = true), this line would reset sawTrustFailure to false, causing the method to incorrectly return TrustLevel.Full instead of TrustLevel.Partial.

To preserve previous failure state, use the OR-assignment operator: sawTrustFailure |= !hasValidSslCertDir;. This ensures that once a failure is detected, it remains true even if subsequent checks pass.

Suggested change
sawTrustFailure = !hasValidSslCertDir;
sawTrustFailure |= !hasValidSslCertDir;

Copilot uses AI. Check for mistakes.
}

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;
}
Comment on lines +994 to 1007
Copy link

Copilot AI Jan 25, 2026

Choose a reason for hiding this comment

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

This change replaces a C# 12+ primary constructor with an explicit constructor and private fields. According to the coding guidelines, the repository uses C# 13 features (LangVersion is set to "preview" in eng/targets/CSharp.Common.props), and primary constructors are supported. The original primary constructor syntax private sealed class NssDb(string path, bool isFirefox) is more concise and idiomatic in modern C#. Unless there's a specific reason for this change (e.g., additional initialization logic needed), the primary constructor should be retained.

Copilot uses AI. Check for mistakes.
}
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
Loading