diff --git a/src/Aspire.Cli/Certificates/CertificateGeneration/UnixCertificateManager.cs b/src/Aspire.Cli/Certificates/CertificateGeneration/UnixCertificateManager.cs index 216daa18287..737bdc35110 100644 --- a/src/Aspire.Cli/Certificates/CertificateGeneration/UnixCertificateManager.cs +++ b/src/Aspire.Cli/Certificates/CertificateGeneration/UnixCertificateManager.cs @@ -5,6 +5,7 @@ using System.Diagnostics; using System.Diagnostics.CodeAnalysis; +using System.Security.Cryptography; using System.Security.Cryptography.X509Certificates; using System.Text.RegularExpressions; using Aspire.Cli; @@ -116,11 +117,19 @@ public override TrustLevel GetTrustLevel(X509Certificate2 certificate) var certPath = Path.Combine(sslCertDir, certificateNickname + ".pem"); if (File.Exists(certPath)) { - using var candidate = X509CertificateLoader.LoadCertificateFromFile(certPath); - if (AreCertificatesEqual(certificate, candidate)) + try { - foundCert = true; - break; + using var candidate = X509CertificateLoader.LoadCertificateFromFile(certPath); + if (AreCertificatesEqual(certificate, candidate)) + { + foundCert = true; + break; + } + } + catch (Exception ex) when (ex is CryptographicException or IOException or UnauthorizedAccessException) + { + // Treat unreadable entries as a miss. A later SSL_CERT_DIR entry may still contain + // the expected certificate, so only report OpenSSL as untrusted after the full search. } } } @@ -476,18 +485,20 @@ protected override void RemoveCertificateFromTrustedRoots(X509Certificate2 certi if (File.Exists(certPath)) { var openSslUntrustSucceeded = false; + var certificateFileDeleted = TryDeleteCertificateFile(certPath); - if (IsCommandAvailable(OpenSslCommand)) + if (certificateFileDeleted) { - if (TryDeleteCertificateFile(certPath) && TryRehashOpenSslCertificates(certDir)) + if (IsCommandAvailable(OpenSslCommand)) + { + openSslUntrustSucceeded = TryRehashOpenSslCertificates(certDir); + } + else { + Log.UnixMissingOpenSslCommand(OpenSslCommand); openSslUntrustSucceeded = true; } } - else - { - Log.UnixMissingOpenSslCommand(OpenSslCommand); - } if (openSslUntrustSucceeded) { diff --git a/src/Aspire.Cli/Resources/DoctorCommandStrings.Designer.cs b/src/Aspire.Cli/Resources/DoctorCommandStrings.Designer.cs index 6b949aff43c..ce63a976622 100644 --- a/src/Aspire.Cli/Resources/DoctorCommandStrings.Designer.cs +++ b/src/Aspire.Cli/Resources/DoctorCommandStrings.Designer.cs @@ -319,6 +319,15 @@ public static string DevCertsCleanAndTrustFixFormat { return ResourceManager.GetString("DevCertsCleanAndTrustFixFormat", resourceCulture); } } + + /// + /// Looks up a localized string similar to Install {0}, run '{1}' to remove all certificates, then run '{2}' to create and trust a new one.. + /// + public static string DevCertsInstallOpenSslCleanAndTrustFixFormat { + get { + return ResourceManager.GetString("DevCertsInstallOpenSslCleanAndTrustFixFormat", resourceCulture); + } + } /// /// Looks up a localized string similar to Multiple HTTPS development certificates found ({0} certificates), but none are trusted. @@ -455,6 +464,60 @@ public static string DevCertsMissingCertUtilFix { } } + /// + /// Looks up a localized string similar to OpenSSL HTTPS development certificate cache is missing the current certificate. + /// + public static string DevCertsOpenSslCacheMissingCurrentCertificateMessage { + get { + return ResourceManager.GetString("DevCertsOpenSslCacheMissingCurrentCertificateMessage", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The OpenSSL certificate cache at '{0}' does not contain certificate {1} from the .NET current user certificate store. Run 'aspire certs clean' and then 'aspire certs trust' to remove stale or corrupt certificates and regenerate trusted development certificates.. + /// + public static string DevCertsOpenSslCacheMissingDetailsFormat { + get { + return ResourceManager.GetString("DevCertsOpenSslCacheMissingDetailsFormat", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to OpenSSL HTTPS development certificate cache contains unreadable certificate files. + /// + public static string DevCertsOpenSslCacheUnreadableMessage { + get { + return ResourceManager.GetString("DevCertsOpenSslCacheUnreadableMessage", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Could not read certificate files under '{0}': {1}. Run 'aspire certs clean' and then 'aspire certs trust' to remove stale or corrupt certificates and regenerate trusted development certificates.. + /// + public static string DevCertsOpenSslCacheUnreadableFilesDetailsFormat { + get { + return ResourceManager.GetString("DevCertsOpenSslCacheUnreadableFilesDetailsFormat", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to OpenSSL HTTPS development certificate cache is missing subject-hash links. + /// + public static string DevCertsOpenSslCacheMissingHashLinkMessage { + get { + return ResourceManager.GetString("DevCertsOpenSslCacheMissingHashLinkMessage", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The OpenSSL certificate cache at '{0}' contains certificate {1}, but no subject-hash entry points to it. OpenSSL workloads use subject-hash entries when loading CA directories.. + /// + public static string DevCertsOpenSslCacheMissingHashLinkDetailsFormat { + get { + return ResourceManager.GetString("DevCertsOpenSslCacheMissingHashLinkDetailsFormat", resourceCulture); + } + } + /// /// Looks up a localized string similar to Developer Control Plane (DCP) bundle not found; skipping connection health checks. /// diff --git a/src/Aspire.Cli/Resources/DoctorCommandStrings.resx b/src/Aspire.Cli/Resources/DoctorCommandStrings.resx index 54c75958aaf..063a3a47e12 100644 --- a/src/Aspire.Cli/Resources/DoctorCommandStrings.resx +++ b/src/Aspire.Cli/Resources/DoctorCommandStrings.resx @@ -155,6 +155,10 @@ Run '{0}' to remove all certificates, then run '{1}' to create and trust a new one. {0} is the aspire certs clean command, {1} is the aspire certs trust command (not localizable) + + Install {0}, run '{1}' to remove all certificates, then run '{2}' to create and trust a new one. + {0} is the openssl command/package name, {1} is the aspire certs clean command, {2} is the aspire certs trust command (not localizable) + Multiple HTTPS development certificates found ({0} certificates), but none are trusted @@ -200,6 +204,27 @@ Install certutil from your distribution's NSS tools package (for example, libnss3-tools). + + OpenSSL HTTPS development certificate cache is missing the current certificate + + + The OpenSSL certificate cache at '{0}' does not contain certificate {1} from the .NET current user certificate store. Run 'aspire certs clean' and then 'aspire certs trust' to remove stale or corrupt certificates and regenerate trusted development certificates. + {0} is the OpenSSL certificate cache directory. {1} is the certificate thumbprint. + + + OpenSSL HTTPS development certificate cache contains unreadable certificate files + + + Could not read certificate files under '{0}': {1}. Run 'aspire certs clean' and then 'aspire certs trust' to remove stale or corrupt certificates and regenerate trusted development certificates. + {0} is the OpenSSL certificate cache directory. {1} is a comma-separated list of certificate file names. + + + OpenSSL HTTPS development certificate cache is missing subject-hash links + + + The OpenSSL certificate cache at '{0}' contains certificate {1}, but no subject-hash entry points to it. OpenSSL workloads use subject-hash entries when loading CA directories. + {0} is the OpenSSL certificate cache directory. {1} is the certificate thumbprint. + Developer Control Plane (DCP) bundle not found; skipping connection health checks diff --git a/src/Aspire.Cli/Resources/xlf/DoctorCommandStrings.cs.xlf b/src/Aspire.Cli/Resources/xlf/DoctorCommandStrings.cs.xlf index 206e4afb801..c0ff9a1963d 100644 --- a/src/Aspire.Cli/Resources/xlf/DoctorCommandStrings.cs.xlf +++ b/src/Aspire.Cli/Resources/xlf/DoctorCommandStrings.cs.xlf @@ -222,6 +222,11 @@ Run '{0}' to remove all certificates, then run '{1}' to create and trust a new one. {0} is the aspire certs clean command, {1} is the aspire certs trust command (not localizable) + + Install {0}, run '{1}' to remove all certificates, then run '{2}' to create and trust a new one. + Install {0}, run '{1}' to remove all certificates, then run '{2}' to create and trust a new one. + {0} is the openssl command/package name, {1} is the aspire certs clean command, {2} is the aspire certs trust command (not localizable) + Aspire uses certutil to query and update NSS certificate databases used by Firefox and Chromium browsers on Linux. Aspire uses certutil to query and update NSS certificate databases used by Firefox and Chromium browsers on Linux. @@ -287,6 +292,36 @@ HTTPS development certificate has an older version ({0}) + + OpenSSL HTTPS development certificate cache is missing the current certificate + OpenSSL HTTPS development certificate cache is missing the current certificate + + + + The OpenSSL certificate cache at '{0}' does not contain certificate {1} from the .NET current user certificate store. Run 'aspire certs clean' and then 'aspire certs trust' to remove stale or corrupt certificates and regenerate trusted development certificates. + The OpenSSL certificate cache at '{0}' does not contain certificate {1} from the .NET current user certificate store. Run 'aspire certs clean' and then 'aspire certs trust' to remove stale or corrupt certificates and regenerate trusted development certificates. + {0} is the OpenSSL certificate cache directory. {1} is the certificate thumbprint. + + + The OpenSSL certificate cache at '{0}' contains certificate {1}, but no subject-hash entry points to it. OpenSSL workloads use subject-hash entries when loading CA directories. + The OpenSSL certificate cache at '{0}' contains certificate {1}, but no subject-hash entry points to it. OpenSSL workloads use subject-hash entries when loading CA directories. + {0} is the OpenSSL certificate cache directory. {1} is the certificate thumbprint. + + + OpenSSL HTTPS development certificate cache is missing subject-hash links + OpenSSL HTTPS development certificate cache is missing subject-hash links + + + + Could not read certificate files under '{0}': {1}. Run 'aspire certs clean' and then 'aspire certs trust' to remove stale or corrupt certificates and regenerate trusted development certificates. + Could not read certificate files under '{0}': {1}. Run 'aspire certs clean' and then 'aspire certs trust' to remove stale or corrupt certificates and regenerate trusted development certificates. + {0} is the OpenSSL certificate cache directory. {1} is a comma-separated list of certificate file names. + + + OpenSSL HTTPS development certificate cache contains unreadable certificate files + OpenSSL HTTPS development certificate cache contains unreadable certificate files + + The certificate is in the trusted store, but SSL_CERT_DIR is not configured to include '{0}'. Some applications may not trust the certificate. 'aspire run' will configure this automatically. The certificate is in the trusted store, but SSL_CERT_DIR is not configured to include '{0}'. Some applications may not trust the certificate. 'aspire run' will configure this automatically. diff --git a/src/Aspire.Cli/Resources/xlf/DoctorCommandStrings.de.xlf b/src/Aspire.Cli/Resources/xlf/DoctorCommandStrings.de.xlf index bb3501ef481..9df825ab628 100644 --- a/src/Aspire.Cli/Resources/xlf/DoctorCommandStrings.de.xlf +++ b/src/Aspire.Cli/Resources/xlf/DoctorCommandStrings.de.xlf @@ -222,6 +222,11 @@ Run '{0}' to remove all certificates, then run '{1}' to create and trust a new one. {0} is the aspire certs clean command, {1} is the aspire certs trust command (not localizable) + + Install {0}, run '{1}' to remove all certificates, then run '{2}' to create and trust a new one. + Install {0}, run '{1}' to remove all certificates, then run '{2}' to create and trust a new one. + {0} is the openssl command/package name, {1} is the aspire certs clean command, {2} is the aspire certs trust command (not localizable) + Aspire uses certutil to query and update NSS certificate databases used by Firefox and Chromium browsers on Linux. Aspire uses certutil to query and update NSS certificate databases used by Firefox and Chromium browsers on Linux. @@ -287,6 +292,36 @@ HTTPS development certificate has an older version ({0}) + + OpenSSL HTTPS development certificate cache is missing the current certificate + OpenSSL HTTPS development certificate cache is missing the current certificate + + + + The OpenSSL certificate cache at '{0}' does not contain certificate {1} from the .NET current user certificate store. Run 'aspire certs clean' and then 'aspire certs trust' to remove stale or corrupt certificates and regenerate trusted development certificates. + The OpenSSL certificate cache at '{0}' does not contain certificate {1} from the .NET current user certificate store. Run 'aspire certs clean' and then 'aspire certs trust' to remove stale or corrupt certificates and regenerate trusted development certificates. + {0} is the OpenSSL certificate cache directory. {1} is the certificate thumbprint. + + + The OpenSSL certificate cache at '{0}' contains certificate {1}, but no subject-hash entry points to it. OpenSSL workloads use subject-hash entries when loading CA directories. + The OpenSSL certificate cache at '{0}' contains certificate {1}, but no subject-hash entry points to it. OpenSSL workloads use subject-hash entries when loading CA directories. + {0} is the OpenSSL certificate cache directory. {1} is the certificate thumbprint. + + + OpenSSL HTTPS development certificate cache is missing subject-hash links + OpenSSL HTTPS development certificate cache is missing subject-hash links + + + + Could not read certificate files under '{0}': {1}. Run 'aspire certs clean' and then 'aspire certs trust' to remove stale or corrupt certificates and regenerate trusted development certificates. + Could not read certificate files under '{0}': {1}. Run 'aspire certs clean' and then 'aspire certs trust' to remove stale or corrupt certificates and regenerate trusted development certificates. + {0} is the OpenSSL certificate cache directory. {1} is a comma-separated list of certificate file names. + + + OpenSSL HTTPS development certificate cache contains unreadable certificate files + OpenSSL HTTPS development certificate cache contains unreadable certificate files + + The certificate is in the trusted store, but SSL_CERT_DIR is not configured to include '{0}'. Some applications may not trust the certificate. 'aspire run' will configure this automatically. The certificate is in the trusted store, but SSL_CERT_DIR is not configured to include '{0}'. Some applications may not trust the certificate. 'aspire run' will configure this automatically. diff --git a/src/Aspire.Cli/Resources/xlf/DoctorCommandStrings.es.xlf b/src/Aspire.Cli/Resources/xlf/DoctorCommandStrings.es.xlf index 70df006cd1a..2a3432fc20a 100644 --- a/src/Aspire.Cli/Resources/xlf/DoctorCommandStrings.es.xlf +++ b/src/Aspire.Cli/Resources/xlf/DoctorCommandStrings.es.xlf @@ -222,6 +222,11 @@ Run '{0}' to remove all certificates, then run '{1}' to create and trust a new one. {0} is the aspire certs clean command, {1} is the aspire certs trust command (not localizable) + + Install {0}, run '{1}' to remove all certificates, then run '{2}' to create and trust a new one. + Install {0}, run '{1}' to remove all certificates, then run '{2}' to create and trust a new one. + {0} is the openssl command/package name, {1} is the aspire certs clean command, {2} is the aspire certs trust command (not localizable) + Aspire uses certutil to query and update NSS certificate databases used by Firefox and Chromium browsers on Linux. Aspire uses certutil to query and update NSS certificate databases used by Firefox and Chromium browsers on Linux. @@ -287,6 +292,36 @@ HTTPS development certificate has an older version ({0}) + + OpenSSL HTTPS development certificate cache is missing the current certificate + OpenSSL HTTPS development certificate cache is missing the current certificate + + + + The OpenSSL certificate cache at '{0}' does not contain certificate {1} from the .NET current user certificate store. Run 'aspire certs clean' and then 'aspire certs trust' to remove stale or corrupt certificates and regenerate trusted development certificates. + The OpenSSL certificate cache at '{0}' does not contain certificate {1} from the .NET current user certificate store. Run 'aspire certs clean' and then 'aspire certs trust' to remove stale or corrupt certificates and regenerate trusted development certificates. + {0} is the OpenSSL certificate cache directory. {1} is the certificate thumbprint. + + + The OpenSSL certificate cache at '{0}' contains certificate {1}, but no subject-hash entry points to it. OpenSSL workloads use subject-hash entries when loading CA directories. + The OpenSSL certificate cache at '{0}' contains certificate {1}, but no subject-hash entry points to it. OpenSSL workloads use subject-hash entries when loading CA directories. + {0} is the OpenSSL certificate cache directory. {1} is the certificate thumbprint. + + + OpenSSL HTTPS development certificate cache is missing subject-hash links + OpenSSL HTTPS development certificate cache is missing subject-hash links + + + + Could not read certificate files under '{0}': {1}. Run 'aspire certs clean' and then 'aspire certs trust' to remove stale or corrupt certificates and regenerate trusted development certificates. + Could not read certificate files under '{0}': {1}. Run 'aspire certs clean' and then 'aspire certs trust' to remove stale or corrupt certificates and regenerate trusted development certificates. + {0} is the OpenSSL certificate cache directory. {1} is a comma-separated list of certificate file names. + + + OpenSSL HTTPS development certificate cache contains unreadable certificate files + OpenSSL HTTPS development certificate cache contains unreadable certificate files + + The certificate is in the trusted store, but SSL_CERT_DIR is not configured to include '{0}'. Some applications may not trust the certificate. 'aspire run' will configure this automatically. The certificate is in the trusted store, but SSL_CERT_DIR is not configured to include '{0}'. Some applications may not trust the certificate. 'aspire run' will configure this automatically. diff --git a/src/Aspire.Cli/Resources/xlf/DoctorCommandStrings.fr.xlf b/src/Aspire.Cli/Resources/xlf/DoctorCommandStrings.fr.xlf index 65b407937f3..cfd900619a7 100644 --- a/src/Aspire.Cli/Resources/xlf/DoctorCommandStrings.fr.xlf +++ b/src/Aspire.Cli/Resources/xlf/DoctorCommandStrings.fr.xlf @@ -222,6 +222,11 @@ Run '{0}' to remove all certificates, then run '{1}' to create and trust a new one. {0} is the aspire certs clean command, {1} is the aspire certs trust command (not localizable) + + Install {0}, run '{1}' to remove all certificates, then run '{2}' to create and trust a new one. + Install {0}, run '{1}' to remove all certificates, then run '{2}' to create and trust a new one. + {0} is the openssl command/package name, {1} is the aspire certs clean command, {2} is the aspire certs trust command (not localizable) + Aspire uses certutil to query and update NSS certificate databases used by Firefox and Chromium browsers on Linux. Aspire uses certutil to query and update NSS certificate databases used by Firefox and Chromium browsers on Linux. @@ -287,6 +292,36 @@ HTTPS development certificate has an older version ({0}) + + OpenSSL HTTPS development certificate cache is missing the current certificate + OpenSSL HTTPS development certificate cache is missing the current certificate + + + + The OpenSSL certificate cache at '{0}' does not contain certificate {1} from the .NET current user certificate store. Run 'aspire certs clean' and then 'aspire certs trust' to remove stale or corrupt certificates and regenerate trusted development certificates. + The OpenSSL certificate cache at '{0}' does not contain certificate {1} from the .NET current user certificate store. Run 'aspire certs clean' and then 'aspire certs trust' to remove stale or corrupt certificates and regenerate trusted development certificates. + {0} is the OpenSSL certificate cache directory. {1} is the certificate thumbprint. + + + The OpenSSL certificate cache at '{0}' contains certificate {1}, but no subject-hash entry points to it. OpenSSL workloads use subject-hash entries when loading CA directories. + The OpenSSL certificate cache at '{0}' contains certificate {1}, but no subject-hash entry points to it. OpenSSL workloads use subject-hash entries when loading CA directories. + {0} is the OpenSSL certificate cache directory. {1} is the certificate thumbprint. + + + OpenSSL HTTPS development certificate cache is missing subject-hash links + OpenSSL HTTPS development certificate cache is missing subject-hash links + + + + Could not read certificate files under '{0}': {1}. Run 'aspire certs clean' and then 'aspire certs trust' to remove stale or corrupt certificates and regenerate trusted development certificates. + Could not read certificate files under '{0}': {1}. Run 'aspire certs clean' and then 'aspire certs trust' to remove stale or corrupt certificates and regenerate trusted development certificates. + {0} is the OpenSSL certificate cache directory. {1} is a comma-separated list of certificate file names. + + + OpenSSL HTTPS development certificate cache contains unreadable certificate files + OpenSSL HTTPS development certificate cache contains unreadable certificate files + + The certificate is in the trusted store, but SSL_CERT_DIR is not configured to include '{0}'. Some applications may not trust the certificate. 'aspire run' will configure this automatically. The certificate is in the trusted store, but SSL_CERT_DIR is not configured to include '{0}'. Some applications may not trust the certificate. 'aspire run' will configure this automatically. diff --git a/src/Aspire.Cli/Resources/xlf/DoctorCommandStrings.it.xlf b/src/Aspire.Cli/Resources/xlf/DoctorCommandStrings.it.xlf index 4d5dbe60a66..cf6f9527adf 100644 --- a/src/Aspire.Cli/Resources/xlf/DoctorCommandStrings.it.xlf +++ b/src/Aspire.Cli/Resources/xlf/DoctorCommandStrings.it.xlf @@ -222,6 +222,11 @@ Run '{0}' to remove all certificates, then run '{1}' to create and trust a new one. {0} is the aspire certs clean command, {1} is the aspire certs trust command (not localizable) + + Install {0}, run '{1}' to remove all certificates, then run '{2}' to create and trust a new one. + Install {0}, run '{1}' to remove all certificates, then run '{2}' to create and trust a new one. + {0} is the openssl command/package name, {1} is the aspire certs clean command, {2} is the aspire certs trust command (not localizable) + Aspire uses certutil to query and update NSS certificate databases used by Firefox and Chromium browsers on Linux. Aspire uses certutil to query and update NSS certificate databases used by Firefox and Chromium browsers on Linux. @@ -287,6 +292,36 @@ HTTPS development certificate has an older version ({0}) + + OpenSSL HTTPS development certificate cache is missing the current certificate + OpenSSL HTTPS development certificate cache is missing the current certificate + + + + The OpenSSL certificate cache at '{0}' does not contain certificate {1} from the .NET current user certificate store. Run 'aspire certs clean' and then 'aspire certs trust' to remove stale or corrupt certificates and regenerate trusted development certificates. + The OpenSSL certificate cache at '{0}' does not contain certificate {1} from the .NET current user certificate store. Run 'aspire certs clean' and then 'aspire certs trust' to remove stale or corrupt certificates and regenerate trusted development certificates. + {0} is the OpenSSL certificate cache directory. {1} is the certificate thumbprint. + + + The OpenSSL certificate cache at '{0}' contains certificate {1}, but no subject-hash entry points to it. OpenSSL workloads use subject-hash entries when loading CA directories. + The OpenSSL certificate cache at '{0}' contains certificate {1}, but no subject-hash entry points to it. OpenSSL workloads use subject-hash entries when loading CA directories. + {0} is the OpenSSL certificate cache directory. {1} is the certificate thumbprint. + + + OpenSSL HTTPS development certificate cache is missing subject-hash links + OpenSSL HTTPS development certificate cache is missing subject-hash links + + + + Could not read certificate files under '{0}': {1}. Run 'aspire certs clean' and then 'aspire certs trust' to remove stale or corrupt certificates and regenerate trusted development certificates. + Could not read certificate files under '{0}': {1}. Run 'aspire certs clean' and then 'aspire certs trust' to remove stale or corrupt certificates and regenerate trusted development certificates. + {0} is the OpenSSL certificate cache directory. {1} is a comma-separated list of certificate file names. + + + OpenSSL HTTPS development certificate cache contains unreadable certificate files + OpenSSL HTTPS development certificate cache contains unreadable certificate files + + The certificate is in the trusted store, but SSL_CERT_DIR is not configured to include '{0}'. Some applications may not trust the certificate. 'aspire run' will configure this automatically. The certificate is in the trusted store, but SSL_CERT_DIR is not configured to include '{0}'. Some applications may not trust the certificate. 'aspire run' will configure this automatically. diff --git a/src/Aspire.Cli/Resources/xlf/DoctorCommandStrings.ja.xlf b/src/Aspire.Cli/Resources/xlf/DoctorCommandStrings.ja.xlf index 4582625018b..4b8a143bd69 100644 --- a/src/Aspire.Cli/Resources/xlf/DoctorCommandStrings.ja.xlf +++ b/src/Aspire.Cli/Resources/xlf/DoctorCommandStrings.ja.xlf @@ -222,6 +222,11 @@ Run '{0}' to remove all certificates, then run '{1}' to create and trust a new one. {0} is the aspire certs clean command, {1} is the aspire certs trust command (not localizable) + + Install {0}, run '{1}' to remove all certificates, then run '{2}' to create and trust a new one. + Install {0}, run '{1}' to remove all certificates, then run '{2}' to create and trust a new one. + {0} is the openssl command/package name, {1} is the aspire certs clean command, {2} is the aspire certs trust command (not localizable) + Aspire uses certutil to query and update NSS certificate databases used by Firefox and Chromium browsers on Linux. Aspire uses certutil to query and update NSS certificate databases used by Firefox and Chromium browsers on Linux. @@ -287,6 +292,36 @@ HTTPS development certificate has an older version ({0}) + + OpenSSL HTTPS development certificate cache is missing the current certificate + OpenSSL HTTPS development certificate cache is missing the current certificate + + + + The OpenSSL certificate cache at '{0}' does not contain certificate {1} from the .NET current user certificate store. Run 'aspire certs clean' and then 'aspire certs trust' to remove stale or corrupt certificates and regenerate trusted development certificates. + The OpenSSL certificate cache at '{0}' does not contain certificate {1} from the .NET current user certificate store. Run 'aspire certs clean' and then 'aspire certs trust' to remove stale or corrupt certificates and regenerate trusted development certificates. + {0} is the OpenSSL certificate cache directory. {1} is the certificate thumbprint. + + + The OpenSSL certificate cache at '{0}' contains certificate {1}, but no subject-hash entry points to it. OpenSSL workloads use subject-hash entries when loading CA directories. + The OpenSSL certificate cache at '{0}' contains certificate {1}, but no subject-hash entry points to it. OpenSSL workloads use subject-hash entries when loading CA directories. + {0} is the OpenSSL certificate cache directory. {1} is the certificate thumbprint. + + + OpenSSL HTTPS development certificate cache is missing subject-hash links + OpenSSL HTTPS development certificate cache is missing subject-hash links + + + + Could not read certificate files under '{0}': {1}. Run 'aspire certs clean' and then 'aspire certs trust' to remove stale or corrupt certificates and regenerate trusted development certificates. + Could not read certificate files under '{0}': {1}. Run 'aspire certs clean' and then 'aspire certs trust' to remove stale or corrupt certificates and regenerate trusted development certificates. + {0} is the OpenSSL certificate cache directory. {1} is a comma-separated list of certificate file names. + + + OpenSSL HTTPS development certificate cache contains unreadable certificate files + OpenSSL HTTPS development certificate cache contains unreadable certificate files + + The certificate is in the trusted store, but SSL_CERT_DIR is not configured to include '{0}'. Some applications may not trust the certificate. 'aspire run' will configure this automatically. The certificate is in the trusted store, but SSL_CERT_DIR is not configured to include '{0}'. Some applications may not trust the certificate. 'aspire run' will configure this automatically. diff --git a/src/Aspire.Cli/Resources/xlf/DoctorCommandStrings.ko.xlf b/src/Aspire.Cli/Resources/xlf/DoctorCommandStrings.ko.xlf index 54099f53d49..c74f8bd57cd 100644 --- a/src/Aspire.Cli/Resources/xlf/DoctorCommandStrings.ko.xlf +++ b/src/Aspire.Cli/Resources/xlf/DoctorCommandStrings.ko.xlf @@ -222,6 +222,11 @@ Run '{0}' to remove all certificates, then run '{1}' to create and trust a new one. {0} is the aspire certs clean command, {1} is the aspire certs trust command (not localizable) + + Install {0}, run '{1}' to remove all certificates, then run '{2}' to create and trust a new one. + Install {0}, run '{1}' to remove all certificates, then run '{2}' to create and trust a new one. + {0} is the openssl command/package name, {1} is the aspire certs clean command, {2} is the aspire certs trust command (not localizable) + Aspire uses certutil to query and update NSS certificate databases used by Firefox and Chromium browsers on Linux. Aspire uses certutil to query and update NSS certificate databases used by Firefox and Chromium browsers on Linux. @@ -287,6 +292,36 @@ HTTPS development certificate has an older version ({0}) + + OpenSSL HTTPS development certificate cache is missing the current certificate + OpenSSL HTTPS development certificate cache is missing the current certificate + + + + The OpenSSL certificate cache at '{0}' does not contain certificate {1} from the .NET current user certificate store. Run 'aspire certs clean' and then 'aspire certs trust' to remove stale or corrupt certificates and regenerate trusted development certificates. + The OpenSSL certificate cache at '{0}' does not contain certificate {1} from the .NET current user certificate store. Run 'aspire certs clean' and then 'aspire certs trust' to remove stale or corrupt certificates and regenerate trusted development certificates. + {0} is the OpenSSL certificate cache directory. {1} is the certificate thumbprint. + + + The OpenSSL certificate cache at '{0}' contains certificate {1}, but no subject-hash entry points to it. OpenSSL workloads use subject-hash entries when loading CA directories. + The OpenSSL certificate cache at '{0}' contains certificate {1}, but no subject-hash entry points to it. OpenSSL workloads use subject-hash entries when loading CA directories. + {0} is the OpenSSL certificate cache directory. {1} is the certificate thumbprint. + + + OpenSSL HTTPS development certificate cache is missing subject-hash links + OpenSSL HTTPS development certificate cache is missing subject-hash links + + + + Could not read certificate files under '{0}': {1}. Run 'aspire certs clean' and then 'aspire certs trust' to remove stale or corrupt certificates and regenerate trusted development certificates. + Could not read certificate files under '{0}': {1}. Run 'aspire certs clean' and then 'aspire certs trust' to remove stale or corrupt certificates and regenerate trusted development certificates. + {0} is the OpenSSL certificate cache directory. {1} is a comma-separated list of certificate file names. + + + OpenSSL HTTPS development certificate cache contains unreadable certificate files + OpenSSL HTTPS development certificate cache contains unreadable certificate files + + The certificate is in the trusted store, but SSL_CERT_DIR is not configured to include '{0}'. Some applications may not trust the certificate. 'aspire run' will configure this automatically. The certificate is in the trusted store, but SSL_CERT_DIR is not configured to include '{0}'. Some applications may not trust the certificate. 'aspire run' will configure this automatically. diff --git a/src/Aspire.Cli/Resources/xlf/DoctorCommandStrings.pl.xlf b/src/Aspire.Cli/Resources/xlf/DoctorCommandStrings.pl.xlf index 75681f5b8c6..fd4bbc8db29 100644 --- a/src/Aspire.Cli/Resources/xlf/DoctorCommandStrings.pl.xlf +++ b/src/Aspire.Cli/Resources/xlf/DoctorCommandStrings.pl.xlf @@ -222,6 +222,11 @@ Run '{0}' to remove all certificates, then run '{1}' to create and trust a new one. {0} is the aspire certs clean command, {1} is the aspire certs trust command (not localizable) + + Install {0}, run '{1}' to remove all certificates, then run '{2}' to create and trust a new one. + Install {0}, run '{1}' to remove all certificates, then run '{2}' to create and trust a new one. + {0} is the openssl command/package name, {1} is the aspire certs clean command, {2} is the aspire certs trust command (not localizable) + Aspire uses certutil to query and update NSS certificate databases used by Firefox and Chromium browsers on Linux. Aspire uses certutil to query and update NSS certificate databases used by Firefox and Chromium browsers on Linux. @@ -287,6 +292,36 @@ HTTPS development certificate has an older version ({0}) + + OpenSSL HTTPS development certificate cache is missing the current certificate + OpenSSL HTTPS development certificate cache is missing the current certificate + + + + The OpenSSL certificate cache at '{0}' does not contain certificate {1} from the .NET current user certificate store. Run 'aspire certs clean' and then 'aspire certs trust' to remove stale or corrupt certificates and regenerate trusted development certificates. + The OpenSSL certificate cache at '{0}' does not contain certificate {1} from the .NET current user certificate store. Run 'aspire certs clean' and then 'aspire certs trust' to remove stale or corrupt certificates and regenerate trusted development certificates. + {0} is the OpenSSL certificate cache directory. {1} is the certificate thumbprint. + + + The OpenSSL certificate cache at '{0}' contains certificate {1}, but no subject-hash entry points to it. OpenSSL workloads use subject-hash entries when loading CA directories. + The OpenSSL certificate cache at '{0}' contains certificate {1}, but no subject-hash entry points to it. OpenSSL workloads use subject-hash entries when loading CA directories. + {0} is the OpenSSL certificate cache directory. {1} is the certificate thumbprint. + + + OpenSSL HTTPS development certificate cache is missing subject-hash links + OpenSSL HTTPS development certificate cache is missing subject-hash links + + + + Could not read certificate files under '{0}': {1}. Run 'aspire certs clean' and then 'aspire certs trust' to remove stale or corrupt certificates and regenerate trusted development certificates. + Could not read certificate files under '{0}': {1}. Run 'aspire certs clean' and then 'aspire certs trust' to remove stale or corrupt certificates and regenerate trusted development certificates. + {0} is the OpenSSL certificate cache directory. {1} is a comma-separated list of certificate file names. + + + OpenSSL HTTPS development certificate cache contains unreadable certificate files + OpenSSL HTTPS development certificate cache contains unreadable certificate files + + The certificate is in the trusted store, but SSL_CERT_DIR is not configured to include '{0}'. Some applications may not trust the certificate. 'aspire run' will configure this automatically. The certificate is in the trusted store, but SSL_CERT_DIR is not configured to include '{0}'. Some applications may not trust the certificate. 'aspire run' will configure this automatically. diff --git a/src/Aspire.Cli/Resources/xlf/DoctorCommandStrings.pt-BR.xlf b/src/Aspire.Cli/Resources/xlf/DoctorCommandStrings.pt-BR.xlf index 35ff2c800f5..459a32262d2 100644 --- a/src/Aspire.Cli/Resources/xlf/DoctorCommandStrings.pt-BR.xlf +++ b/src/Aspire.Cli/Resources/xlf/DoctorCommandStrings.pt-BR.xlf @@ -222,6 +222,11 @@ Run '{0}' to remove all certificates, then run '{1}' to create and trust a new one. {0} is the aspire certs clean command, {1} is the aspire certs trust command (not localizable) + + Install {0}, run '{1}' to remove all certificates, then run '{2}' to create and trust a new one. + Install {0}, run '{1}' to remove all certificates, then run '{2}' to create and trust a new one. + {0} is the openssl command/package name, {1} is the aspire certs clean command, {2} is the aspire certs trust command (not localizable) + Aspire uses certutil to query and update NSS certificate databases used by Firefox and Chromium browsers on Linux. Aspire uses certutil to query and update NSS certificate databases used by Firefox and Chromium browsers on Linux. @@ -287,6 +292,36 @@ HTTPS development certificate has an older version ({0}) + + OpenSSL HTTPS development certificate cache is missing the current certificate + OpenSSL HTTPS development certificate cache is missing the current certificate + + + + The OpenSSL certificate cache at '{0}' does not contain certificate {1} from the .NET current user certificate store. Run 'aspire certs clean' and then 'aspire certs trust' to remove stale or corrupt certificates and regenerate trusted development certificates. + The OpenSSL certificate cache at '{0}' does not contain certificate {1} from the .NET current user certificate store. Run 'aspire certs clean' and then 'aspire certs trust' to remove stale or corrupt certificates and regenerate trusted development certificates. + {0} is the OpenSSL certificate cache directory. {1} is the certificate thumbprint. + + + The OpenSSL certificate cache at '{0}' contains certificate {1}, but no subject-hash entry points to it. OpenSSL workloads use subject-hash entries when loading CA directories. + The OpenSSL certificate cache at '{0}' contains certificate {1}, but no subject-hash entry points to it. OpenSSL workloads use subject-hash entries when loading CA directories. + {0} is the OpenSSL certificate cache directory. {1} is the certificate thumbprint. + + + OpenSSL HTTPS development certificate cache is missing subject-hash links + OpenSSL HTTPS development certificate cache is missing subject-hash links + + + + Could not read certificate files under '{0}': {1}. Run 'aspire certs clean' and then 'aspire certs trust' to remove stale or corrupt certificates and regenerate trusted development certificates. + Could not read certificate files under '{0}': {1}. Run 'aspire certs clean' and then 'aspire certs trust' to remove stale or corrupt certificates and regenerate trusted development certificates. + {0} is the OpenSSL certificate cache directory. {1} is a comma-separated list of certificate file names. + + + OpenSSL HTTPS development certificate cache contains unreadable certificate files + OpenSSL HTTPS development certificate cache contains unreadable certificate files + + The certificate is in the trusted store, but SSL_CERT_DIR is not configured to include '{0}'. Some applications may not trust the certificate. 'aspire run' will configure this automatically. The certificate is in the trusted store, but SSL_CERT_DIR is not configured to include '{0}'. Some applications may not trust the certificate. 'aspire run' will configure this automatically. diff --git a/src/Aspire.Cli/Resources/xlf/DoctorCommandStrings.ru.xlf b/src/Aspire.Cli/Resources/xlf/DoctorCommandStrings.ru.xlf index 97943c9f61e..bb05e691ceb 100644 --- a/src/Aspire.Cli/Resources/xlf/DoctorCommandStrings.ru.xlf +++ b/src/Aspire.Cli/Resources/xlf/DoctorCommandStrings.ru.xlf @@ -222,6 +222,11 @@ Run '{0}' to remove all certificates, then run '{1}' to create and trust a new one. {0} is the aspire certs clean command, {1} is the aspire certs trust command (not localizable) + + Install {0}, run '{1}' to remove all certificates, then run '{2}' to create and trust a new one. + Install {0}, run '{1}' to remove all certificates, then run '{2}' to create and trust a new one. + {0} is the openssl command/package name, {1} is the aspire certs clean command, {2} is the aspire certs trust command (not localizable) + Aspire uses certutil to query and update NSS certificate databases used by Firefox and Chromium browsers on Linux. Aspire uses certutil to query and update NSS certificate databases used by Firefox and Chromium browsers on Linux. @@ -287,6 +292,36 @@ HTTPS development certificate has an older version ({0}) + + OpenSSL HTTPS development certificate cache is missing the current certificate + OpenSSL HTTPS development certificate cache is missing the current certificate + + + + The OpenSSL certificate cache at '{0}' does not contain certificate {1} from the .NET current user certificate store. Run 'aspire certs clean' and then 'aspire certs trust' to remove stale or corrupt certificates and regenerate trusted development certificates. + The OpenSSL certificate cache at '{0}' does not contain certificate {1} from the .NET current user certificate store. Run 'aspire certs clean' and then 'aspire certs trust' to remove stale or corrupt certificates and regenerate trusted development certificates. + {0} is the OpenSSL certificate cache directory. {1} is the certificate thumbprint. + + + The OpenSSL certificate cache at '{0}' contains certificate {1}, but no subject-hash entry points to it. OpenSSL workloads use subject-hash entries when loading CA directories. + The OpenSSL certificate cache at '{0}' contains certificate {1}, but no subject-hash entry points to it. OpenSSL workloads use subject-hash entries when loading CA directories. + {0} is the OpenSSL certificate cache directory. {1} is the certificate thumbprint. + + + OpenSSL HTTPS development certificate cache is missing subject-hash links + OpenSSL HTTPS development certificate cache is missing subject-hash links + + + + Could not read certificate files under '{0}': {1}. Run 'aspire certs clean' and then 'aspire certs trust' to remove stale or corrupt certificates and regenerate trusted development certificates. + Could not read certificate files under '{0}': {1}. Run 'aspire certs clean' and then 'aspire certs trust' to remove stale or corrupt certificates and regenerate trusted development certificates. + {0} is the OpenSSL certificate cache directory. {1} is a comma-separated list of certificate file names. + + + OpenSSL HTTPS development certificate cache contains unreadable certificate files + OpenSSL HTTPS development certificate cache contains unreadable certificate files + + The certificate is in the trusted store, but SSL_CERT_DIR is not configured to include '{0}'. Some applications may not trust the certificate. 'aspire run' will configure this automatically. The certificate is in the trusted store, but SSL_CERT_DIR is not configured to include '{0}'. Some applications may not trust the certificate. 'aspire run' will configure this automatically. diff --git a/src/Aspire.Cli/Resources/xlf/DoctorCommandStrings.tr.xlf b/src/Aspire.Cli/Resources/xlf/DoctorCommandStrings.tr.xlf index b792b1cf1f2..382a4e311b3 100644 --- a/src/Aspire.Cli/Resources/xlf/DoctorCommandStrings.tr.xlf +++ b/src/Aspire.Cli/Resources/xlf/DoctorCommandStrings.tr.xlf @@ -222,6 +222,11 @@ Run '{0}' to remove all certificates, then run '{1}' to create and trust a new one. {0} is the aspire certs clean command, {1} is the aspire certs trust command (not localizable) + + Install {0}, run '{1}' to remove all certificates, then run '{2}' to create and trust a new one. + Install {0}, run '{1}' to remove all certificates, then run '{2}' to create and trust a new one. + {0} is the openssl command/package name, {1} is the aspire certs clean command, {2} is the aspire certs trust command (not localizable) + Aspire uses certutil to query and update NSS certificate databases used by Firefox and Chromium browsers on Linux. Aspire uses certutil to query and update NSS certificate databases used by Firefox and Chromium browsers on Linux. @@ -287,6 +292,36 @@ HTTPS development certificate has an older version ({0}) + + OpenSSL HTTPS development certificate cache is missing the current certificate + OpenSSL HTTPS development certificate cache is missing the current certificate + + + + The OpenSSL certificate cache at '{0}' does not contain certificate {1} from the .NET current user certificate store. Run 'aspire certs clean' and then 'aspire certs trust' to remove stale or corrupt certificates and regenerate trusted development certificates. + The OpenSSL certificate cache at '{0}' does not contain certificate {1} from the .NET current user certificate store. Run 'aspire certs clean' and then 'aspire certs trust' to remove stale or corrupt certificates and regenerate trusted development certificates. + {0} is the OpenSSL certificate cache directory. {1} is the certificate thumbprint. + + + The OpenSSL certificate cache at '{0}' contains certificate {1}, but no subject-hash entry points to it. OpenSSL workloads use subject-hash entries when loading CA directories. + The OpenSSL certificate cache at '{0}' contains certificate {1}, but no subject-hash entry points to it. OpenSSL workloads use subject-hash entries when loading CA directories. + {0} is the OpenSSL certificate cache directory. {1} is the certificate thumbprint. + + + OpenSSL HTTPS development certificate cache is missing subject-hash links + OpenSSL HTTPS development certificate cache is missing subject-hash links + + + + Could not read certificate files under '{0}': {1}. Run 'aspire certs clean' and then 'aspire certs trust' to remove stale or corrupt certificates and regenerate trusted development certificates. + Could not read certificate files under '{0}': {1}. Run 'aspire certs clean' and then 'aspire certs trust' to remove stale or corrupt certificates and regenerate trusted development certificates. + {0} is the OpenSSL certificate cache directory. {1} is a comma-separated list of certificate file names. + + + OpenSSL HTTPS development certificate cache contains unreadable certificate files + OpenSSL HTTPS development certificate cache contains unreadable certificate files + + The certificate is in the trusted store, but SSL_CERT_DIR is not configured to include '{0}'. Some applications may not trust the certificate. 'aspire run' will configure this automatically. The certificate is in the trusted store, but SSL_CERT_DIR is not configured to include '{0}'. Some applications may not trust the certificate. 'aspire run' will configure this automatically. diff --git a/src/Aspire.Cli/Resources/xlf/DoctorCommandStrings.zh-Hans.xlf b/src/Aspire.Cli/Resources/xlf/DoctorCommandStrings.zh-Hans.xlf index 3a781342b2e..3440d39775e 100644 --- a/src/Aspire.Cli/Resources/xlf/DoctorCommandStrings.zh-Hans.xlf +++ b/src/Aspire.Cli/Resources/xlf/DoctorCommandStrings.zh-Hans.xlf @@ -222,6 +222,11 @@ Run '{0}' to remove all certificates, then run '{1}' to create and trust a new one. {0} is the aspire certs clean command, {1} is the aspire certs trust command (not localizable) + + Install {0}, run '{1}' to remove all certificates, then run '{2}' to create and trust a new one. + Install {0}, run '{1}' to remove all certificates, then run '{2}' to create and trust a new one. + {0} is the openssl command/package name, {1} is the aspire certs clean command, {2} is the aspire certs trust command (not localizable) + Aspire uses certutil to query and update NSS certificate databases used by Firefox and Chromium browsers on Linux. Aspire uses certutil to query and update NSS certificate databases used by Firefox and Chromium browsers on Linux. @@ -287,6 +292,36 @@ HTTPS development certificate has an older version ({0}) + + OpenSSL HTTPS development certificate cache is missing the current certificate + OpenSSL HTTPS development certificate cache is missing the current certificate + + + + The OpenSSL certificate cache at '{0}' does not contain certificate {1} from the .NET current user certificate store. Run 'aspire certs clean' and then 'aspire certs trust' to remove stale or corrupt certificates and regenerate trusted development certificates. + The OpenSSL certificate cache at '{0}' does not contain certificate {1} from the .NET current user certificate store. Run 'aspire certs clean' and then 'aspire certs trust' to remove stale or corrupt certificates and regenerate trusted development certificates. + {0} is the OpenSSL certificate cache directory. {1} is the certificate thumbprint. + + + The OpenSSL certificate cache at '{0}' contains certificate {1}, but no subject-hash entry points to it. OpenSSL workloads use subject-hash entries when loading CA directories. + The OpenSSL certificate cache at '{0}' contains certificate {1}, but no subject-hash entry points to it. OpenSSL workloads use subject-hash entries when loading CA directories. + {0} is the OpenSSL certificate cache directory. {1} is the certificate thumbprint. + + + OpenSSL HTTPS development certificate cache is missing subject-hash links + OpenSSL HTTPS development certificate cache is missing subject-hash links + + + + Could not read certificate files under '{0}': {1}. Run 'aspire certs clean' and then 'aspire certs trust' to remove stale or corrupt certificates and regenerate trusted development certificates. + Could not read certificate files under '{0}': {1}. Run 'aspire certs clean' and then 'aspire certs trust' to remove stale or corrupt certificates and regenerate trusted development certificates. + {0} is the OpenSSL certificate cache directory. {1} is a comma-separated list of certificate file names. + + + OpenSSL HTTPS development certificate cache contains unreadable certificate files + OpenSSL HTTPS development certificate cache contains unreadable certificate files + + The certificate is in the trusted store, but SSL_CERT_DIR is not configured to include '{0}'. Some applications may not trust the certificate. 'aspire run' will configure this automatically. The certificate is in the trusted store, but SSL_CERT_DIR is not configured to include '{0}'. Some applications may not trust the certificate. 'aspire run' will configure this automatically. diff --git a/src/Aspire.Cli/Resources/xlf/DoctorCommandStrings.zh-Hant.xlf b/src/Aspire.Cli/Resources/xlf/DoctorCommandStrings.zh-Hant.xlf index 5cde62f77f6..bc6c8d5e2bc 100644 --- a/src/Aspire.Cli/Resources/xlf/DoctorCommandStrings.zh-Hant.xlf +++ b/src/Aspire.Cli/Resources/xlf/DoctorCommandStrings.zh-Hant.xlf @@ -222,6 +222,11 @@ Run '{0}' to remove all certificates, then run '{1}' to create and trust a new one. {0} is the aspire certs clean command, {1} is the aspire certs trust command (not localizable) + + Install {0}, run '{1}' to remove all certificates, then run '{2}' to create and trust a new one. + Install {0}, run '{1}' to remove all certificates, then run '{2}' to create and trust a new one. + {0} is the openssl command/package name, {1} is the aspire certs clean command, {2} is the aspire certs trust command (not localizable) + Aspire uses certutil to query and update NSS certificate databases used by Firefox and Chromium browsers on Linux. Aspire uses certutil to query and update NSS certificate databases used by Firefox and Chromium browsers on Linux. @@ -287,6 +292,36 @@ HTTPS development certificate has an older version ({0}) + + OpenSSL HTTPS development certificate cache is missing the current certificate + OpenSSL HTTPS development certificate cache is missing the current certificate + + + + The OpenSSL certificate cache at '{0}' does not contain certificate {1} from the .NET current user certificate store. Run 'aspire certs clean' and then 'aspire certs trust' to remove stale or corrupt certificates and regenerate trusted development certificates. + The OpenSSL certificate cache at '{0}' does not contain certificate {1} from the .NET current user certificate store. Run 'aspire certs clean' and then 'aspire certs trust' to remove stale or corrupt certificates and regenerate trusted development certificates. + {0} is the OpenSSL certificate cache directory. {1} is the certificate thumbprint. + + + The OpenSSL certificate cache at '{0}' contains certificate {1}, but no subject-hash entry points to it. OpenSSL workloads use subject-hash entries when loading CA directories. + The OpenSSL certificate cache at '{0}' contains certificate {1}, but no subject-hash entry points to it. OpenSSL workloads use subject-hash entries when loading CA directories. + {0} is the OpenSSL certificate cache directory. {1} is the certificate thumbprint. + + + OpenSSL HTTPS development certificate cache is missing subject-hash links + OpenSSL HTTPS development certificate cache is missing subject-hash links + + + + Could not read certificate files under '{0}': {1}. Run 'aspire certs clean' and then 'aspire certs trust' to remove stale or corrupt certificates and regenerate trusted development certificates. + Could not read certificate files under '{0}': {1}. Run 'aspire certs clean' and then 'aspire certs trust' to remove stale or corrupt certificates and regenerate trusted development certificates. + {0} is the OpenSSL certificate cache directory. {1} is a comma-separated list of certificate file names. + + + OpenSSL HTTPS development certificate cache contains unreadable certificate files + OpenSSL HTTPS development certificate cache contains unreadable certificate files + + The certificate is in the trusted store, but SSL_CERT_DIR is not configured to include '{0}'. Some applications may not trust the certificate. 'aspire run' will configure this automatically. The certificate is in the trusted store, but SSL_CERT_DIR is not configured to include '{0}'. Some applications may not trust the certificate. 'aspire run' will configure this automatically. diff --git a/src/Aspire.Cli/Utils/EnvironmentChecker/DevCertsCheck.cs b/src/Aspire.Cli/Utils/EnvironmentChecker/DevCertsCheck.cs index bd98a403ea4..eddc0790f95 100644 --- a/src/Aspire.Cli/Utils/EnvironmentChecker/DevCertsCheck.cs +++ b/src/Aspire.Cli/Utils/EnvironmentChecker/DevCertsCheck.cs @@ -2,8 +2,12 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Globalization; +using System.Security.Cryptography; +using System.Security.Cryptography.X509Certificates; +using System.Text; using System.Text.Json.Nodes; using Aspire.Cli.Certificates; +using Aspire.Cli.DotNet; using Aspire.Cli.Resources; using Microsoft.AspNetCore.Certificates.Generation; using Microsoft.Extensions.Logging; @@ -13,38 +17,47 @@ namespace Aspire.Cli.Utils.EnvironmentChecker; /// /// Checks if the HTTPS development certificate is trusted and detects multiple certificates. /// -internal sealed class DevCertsCheck(ILogger logger, ICertificateToolRunner certificateToolRunner, IEnvironment environment) : IEnvironmentCheck +internal sealed class DevCertsCheck(ILogger logger, ICertificateToolRunner certificateToolRunner, IEnvironment environment, IProcessExecutionFactory processExecutionFactory) : IEnvironmentCheck { internal const string CheckName = "dev-certs"; internal const string VersionCheckName = "dev-certs-version"; internal const string CertUtilCheckName = "dev-certs-certutil"; + internal const string OpenSslCertificateCacheCheckName = "dev-certs-openssl-cache"; + private const string OpenSslCommand = "openssl"; + private const int OpenSslHashCollisionSearchLimit = 10; public int Order => 35; // After SDK check (30), before container checks (40+) private static readonly string s_trustFixCommand = string.Format(CultureInfo.InvariantCulture, DoctorCommandStrings.DevCertsTrustFixFormat, "aspire certs trust"); private static readonly string s_cleanAndTrustFixCommand = string.Format(CultureInfo.InvariantCulture, DoctorCommandStrings.DevCertsCleanAndTrustFixFormat, "aspire certs clean", "aspire certs trust"); + private static readonly string s_installOpenSslCleanAndTrustFixCommand = string.Format(CultureInfo.InvariantCulture, DoctorCommandStrings.DevCertsInstallOpenSslCleanAndTrustFixFormat, "openssl", "aspire certs clean", "aspire certs trust"); - public Task> CheckAsync(CancellationToken cancellationToken = default) + public async Task> CheckAsync(CancellationToken cancellationToken = default) { try { var trustResult = certificateToolRunner.CheckHttpCertificate(); var results = EvaluateCertificateResults(trustResult.Certificates, environment); + await AddLinuxOpenSslCertificateCacheWarningsAsync(results, trustResult.Certificates, environment, cancellationToken).ConfigureAwait(false); AddLinuxCertificateToolWarnings(results, environment); - return Task.FromResult>(results); + return results; + } + catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested) + { + throw; } catch (Exception ex) { logger.LogDebug(ex, "Error checking dev-certs"); - return Task.FromResult>([new EnvironmentCheckResult + return [new EnvironmentCheckResult { Category = EnvironmentCheckCategories.Environment, Name = CheckName, Status = EnvironmentCheckStatus.Warning, Message = "Unable to check HTTPS development certificate", Details = ex.Message - }]); + }]; } } @@ -203,6 +216,278 @@ internal static List EvaluateCertificateResults( return results; } + private async Task AddLinuxOpenSslCertificateCacheWarningsAsync(List results, IReadOnlyList certInfos, IEnvironment environment, CancellationToken cancellationToken) + { + if (!environment.IsLinux()) + { + return; + } + + var currentCertificates = GetCurrentDevCertificates(certInfos).ToList(); + if (currentCertificates.Count == 0) + { + return; + } + + var trustPath = CertificateHelpers.GetDevCertsTrustPath(environment); + var environmentVariables = GetEnvironmentVariables(environment); + PathLookupHelper.TryResolveExecutablePath(OpenSslCommand, out var openSslPath, environmentVariables); + + var cacheStatus = await EvaluateOpenSslCertificateCacheAsync(trustPath, currentCertificates, openSslPath, cancellationToken).ConfigureAwait(false); + if (cacheStatus is null) + { + return; + } + + results.Add(new EnvironmentCheckResult + { + Category = EnvironmentCheckCategories.Environment, + Name = OpenSslCertificateCacheCheckName, + Status = EnvironmentCheckStatus.Warning, + Message = cacheStatus.Message, + Details = cacheStatus.Details, + Fix = cacheStatus.Fix, + Link = "https://aka.ms/aspire-prerequisites#dev-certs" + }); + } + + private static IEnumerable GetCurrentDevCertificates(IReadOnlyList certInfos) + { + var now = DateTimeOffset.Now; + return certInfos + .Where(c => c.IsHttpsDevelopmentCertificate && + c.ValidityNotBefore <= now && + now <= c.ValidityNotAfter && + !string.IsNullOrEmpty(c.Thumbprint)) + .OrderByDescending(c => c.Version) + .ThenByDescending(c => c.ValidityNotAfter) + .ThenBy(c => c.Thumbprint, StringComparer.OrdinalIgnoreCase); + } + + private async Task EvaluateOpenSslCertificateCacheAsync(string trustPath, IReadOnlyList currentCertificates, string? openSslPath, CancellationToken cancellationToken) + { + var fix = GetOpenSslCertificateCacheFix(openSslPath); + + if (!Directory.Exists(trustPath)) + { + var trustedThumbprints = GetTrustedThumbprints(currentCertificates); + if (trustedThumbprints.Count == 0) + { + return null; + } + + return new( + DoctorCommandStrings.DevCertsOpenSslCacheMissingCurrentCertificateMessage, + string.Format(CultureInfo.CurrentCulture, DoctorCommandStrings.DevCertsOpenSslCacheMissingDetailsFormat, trustPath, string.Join(", ", trustedThumbprints)), + fix); + } + + var unreadableFiles = new List(); + var mismatchedThumbprints = new List(); + var missingTrustedThumbprints = new List(); + var missingHashLinkThumbprints = new List(); + + foreach (var certificate in currentCertificates) + { + var certificateFileName = GetOpenSslCertificateFileName(certificate.Thumbprint!); + var certificateFile = Path.Combine(trustPath, certificateFileName); + if (!File.Exists(certificateFile)) + { + if (certificate.TrustLevel != CertificateManager.TrustLevel.None) + { + missingTrustedThumbprints.Add(certificate.Thumbprint!); + } + + continue; + } + + try + { + using var cachedCertificate = X509CertificateLoader.LoadCertificateFromFile(certificateFile); + if (!string.Equals(certificate.Thumbprint, cachedCertificate.Thumbprint, StringComparison.OrdinalIgnoreCase)) + { + mismatchedThumbprints.Add(certificate.Thumbprint!); + } + else if (certificate.TrustLevel != CertificateManager.TrustLevel.None && + !await HasOpenSslHashEntryAsync(trustPath, certificateFile, cachedCertificate, openSslPath, cancellationToken).ConfigureAwait(false)) + { + missingHashLinkThumbprints.Add(certificate.Thumbprint!); + } + } + catch (Exception ex) when (ex is CryptographicException or IOException or UnauthorizedAccessException) + { + unreadableFiles.Add(certificateFileName); + } + } + + if (unreadableFiles.Count > 0) + { + return new( + DoctorCommandStrings.DevCertsOpenSslCacheUnreadableMessage, + string.Format(CultureInfo.CurrentCulture, DoctorCommandStrings.DevCertsOpenSslCacheUnreadableFilesDetailsFormat, trustPath, string.Join(", ", unreadableFiles)), + fix); + } + + if (mismatchedThumbprints.Count > 0) + { + return new( + DoctorCommandStrings.DevCertsOpenSslCacheMissingCurrentCertificateMessage, + string.Format(CultureInfo.CurrentCulture, DoctorCommandStrings.DevCertsOpenSslCacheMissingDetailsFormat, trustPath, string.Join(", ", mismatchedThumbprints)), + fix); + } + + if (missingTrustedThumbprints.Count > 0) + { + return new( + DoctorCommandStrings.DevCertsOpenSslCacheMissingCurrentCertificateMessage, + string.Format(CultureInfo.CurrentCulture, DoctorCommandStrings.DevCertsOpenSslCacheMissingDetailsFormat, trustPath, string.Join(", ", missingTrustedThumbprints)), + fix); + } + + if (missingHashLinkThumbprints.Count > 0) + { + return new( + DoctorCommandStrings.DevCertsOpenSslCacheMissingHashLinkMessage, + string.Format(CultureInfo.CurrentCulture, DoctorCommandStrings.DevCertsOpenSslCacheMissingHashLinkDetailsFormat, trustPath, string.Join(", ", missingHashLinkThumbprints)), + fix); + } + + return null; + } + + private static string GetOpenSslCertificateCacheFix(string? openSslPath) => + openSslPath is null ? s_installOpenSslCleanAndTrustFixCommand : s_cleanAndTrustFixCommand; + + private async Task HasOpenSslHashEntryAsync(string trustPath, string certificateFile, X509Certificate2 certificate, string? openSslPath, CancellationToken cancellationToken) + { + if (openSslPath is not null) + { + var (success, hash) = await TryGetOpenSslHashAsync(openSslPath, certificateFile, cancellationToken).ConfigureAwait(false); + return success && + HasMatchingHashEntry(trustPath, hash, certificate); + } + + // Without openssl we cannot compute the subject hash that OpenSSL requires, so + // friendly-name PEM checks are the strongest validation we can perform. + return true; + } + + private static bool HasMatchingHashEntry(string trustPath, string hash, X509Certificate2 certificate) + { + for (var i = 0; i < OpenSslHashCollisionSearchLimit; i++) + { + var hashEntryPath = Path.Combine(trustPath, $"{hash}.{i}"); + if (CertificateFileMatches(hashEntryPath, certificate)) + { + return true; + } + } + + return false; + } + + private static bool CertificateFileMatches(string certificateFile, X509Certificate2 certificate) + { + if (!File.Exists(certificateFile)) + { + return false; + } + + try + { + using var cachedCertificate = X509CertificateLoader.LoadCertificateFromFile(certificateFile); + + return string.Equals(certificate.Thumbprint, cachedCertificate.Thumbprint, StringComparison.OrdinalIgnoreCase); + } + catch (Exception ex) when (ex is CryptographicException or IOException or UnauthorizedAccessException) + { + return false; + } + } + + private async Task<(bool Success, string Hash)> TryGetOpenSslHashAsync(string openSslPath, string certificateFile, CancellationToken cancellationToken) + { + var stdout = new StringBuilder(); + var workingDirectory = Path.GetDirectoryName(certificateFile) is { } directory + ? new DirectoryInfo(directory) + : new DirectoryInfo(Directory.GetCurrentDirectory()); + await using var process = processExecutionFactory.CreateExecution( + openSslPath, + ["x509", "-hash", "-noout", "-in", certificateFile], + env: null, + workingDirectory, + new ProcessInvocationOptions + { + SuppressLogging = true, + StandardOutputCallback = line => stdout.AppendLine(line), + StandardErrorCallback = _ => { }, + }); + var started = false; + + try + { + started = await process.StartAsync(cancellationToken).ConfigureAwait(false); + if (!started) + { + return (false, ""); + } + + var exitCode = await process.WaitForExitAsync(cancellationToken).ConfigureAwait(false); + if (exitCode != 0) + { + return (false, ""); + } + + var hash = stdout.ToString().Trim(); + return hash.Length > 0 ? (true, hash) : (false, ""); + } + catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested) + { + if (started) + { + TryKillProcess(process); + } + + throw; + } + catch + { + if (started) + { + TryKillProcess(process); + } + + return (false, ""); + } + } + + private static void TryKillProcess(IProcessExecution process) + { + try + { + if (!process.HasExited) + { + process.Kill(entireProcessTree: true); + } + } + catch (Exception) + { + // Process cleanup is best-effort; do not let a kill failure mask caller cancellation + // or turn a non-critical OpenSSL probe failure into a failed doctor check. + } + } + + private static List GetTrustedThumbprints(IReadOnlyList currentCertificates) + { + return currentCertificates + .Where(c => c.TrustLevel != CertificateManager.TrustLevel.None) + .Select(c => c.Thumbprint!) + .ToList(); + } + + private static string GetOpenSslCertificateFileName(string certificateThumbprint) => + $"aspnetcore-localhost-{certificateThumbprint}.pem"; + private static void AddLinuxCertificateToolWarnings(List results, IEnvironment environment) { if (!environment.IsLinux()) @@ -210,9 +495,7 @@ private static void AddLinuxCertificateToolWarnings(List return; } - var environmentVariables = environment.GetEnvironmentVariables() - .Where(kv => kv.Value is not null) - .ToDictionary(kv => kv.Name, kv => kv.Value!); + var environmentVariables = GetEnvironmentVariables(environment); if (PathLookupHelper.TryResolveExecutablePath(CertificateHelpers.CertUtilCommand, out _, environmentVariables)) { @@ -231,6 +514,11 @@ private static void AddLinuxCertificateToolWarnings(List }); } + private static Dictionary GetEnvironmentVariables(IEnvironment environment) => + environment.GetEnvironmentVariables() + .Where(kv => kv.Value is not null) + .ToDictionary(kv => kv.Name, kv => kv.Value!); + /// /// Builds structured metadata from certificate information for JSON output. /// @@ -288,4 +576,6 @@ private static string BuildSslCertDirFixCommand(string devCertsTrustPath, IEnvir // We still prepend $SSL_CERT_DIR to be safe in case the user makes later modifications to their environment return $"export SSL_CERT_DIR=\"$SSL_CERT_DIR:{string.Join(':', systemCertDirs)}\""; } + + private sealed record OpenSslCertificateCacheStatus(string Message, string Details, string Fix); } diff --git a/src/Aspire.Hosting/ApplicationModel/CertificateTrustExecutionConfigurationGatherer.cs b/src/Aspire.Hosting/ApplicationModel/CertificateTrustExecutionConfigurationGatherer.cs index 2041a84c68e..9ea0bd78f78 100644 --- a/src/Aspire.Hosting/ApplicationModel/CertificateTrustExecutionConfigurationGatherer.cs +++ b/src/Aspire.Hosting/ApplicationModel/CertificateTrustExecutionConfigurationGatherer.cs @@ -16,6 +16,9 @@ namespace Aspire.Hosting.ApplicationModel; /// internal class CertificateTrustExecutionConfigurationGatherer : IExecutionConfigurationGatherer { + internal const string SslCertDirEnvironmentVariable = "SSL_CERT_DIR"; + internal const string SslCertFileEnvironmentVariable = "SSL_CERT_FILE"; + private readonly Func _configContextFactory; /// @@ -83,11 +86,11 @@ public async ValueTask GatherAsync(IExecutionConfigurationGathererContext contex var configurationContext = _configContextFactory(additionalData.Scope); // Apply default OpenSSL environment configuration for certificate trust - context.EnvironmentVariables["SSL_CERT_DIR"] = configurationContext.CertificateDirectoriesPath; + context.EnvironmentVariables[SslCertDirEnvironmentVariable] = configurationContext.CertificateDirectoriesPath; if (additionalData.Scope != CertificateTrustScope.Append) { - context.EnvironmentVariables["SSL_CERT_FILE"] = configurationContext.CertificateBundlePath; + context.EnvironmentVariables[SslCertFileEnvironmentVariable] = configurationContext.CertificateBundlePath; } var callbackContext = new CertificateTrustConfigurationCallbackAnnotationContext diff --git a/src/Aspire.Hosting/Dcp/ExecutableCreator.cs b/src/Aspire.Hosting/Dcp/ExecutableCreator.cs index fa51a923884..07f17270f17 100644 --- a/src/Aspire.Hosting/Dcp/ExecutableCreator.cs +++ b/src/Aspire.Hosting/Dcp/ExecutableCreator.cs @@ -501,10 +501,17 @@ private async Task BuildExecutableConfiguration(Rendere var dirs = new List { certificatesOutputPath }; if (scope == CertificateTrustScope.Append) { - var existing = Environment.GetEnvironmentVariable("SSL_CERT_DIR"); - if (!string.IsNullOrEmpty(existing)) + var existingSslCertDir = Environment.GetEnvironmentVariable(CertificateTrustExecutionConfigurationGatherer.SslCertDirEnvironmentVariable); + if (existingSslCertDir is not null) { - dirs.AddRange(existing.Split(Path.PathSeparator, StringSplitOptions.RemoveEmptyEntries)); + dirs.AddRange(existingSslCertDir.Split(Path.PathSeparator, StringSplitOptions.RemoveEmptyEntries)); + } + else if (OperatingSystem.IsLinux()) + { + // Do not invoke the openssl CLI here. This fallback is only for dotnet-run AppHosts + // where Aspire CLI did not already materialize OpenSSL's default directory into + // SSL_CERT_DIR, so reuse the same well-known certificate directories used for containers. + dirs.AddRange(ContainerCertificatePathsAnnotation.DefaultCertificateDirectoriesPaths.Where(Directory.Exists)); } } diff --git a/tests/Aspire.Cli.Tests/Certificates/UnixCertificateManagerTests.cs b/tests/Aspire.Cli.Tests/Certificates/UnixCertificateManagerTests.cs index cdcd85d6efe..f3a01288f8c 100644 --- a/tests/Aspire.Cli.Tests/Certificates/UnixCertificateManagerTests.cs +++ b/tests/Aspire.Cli.Tests/Certificates/UnixCertificateManagerTests.cs @@ -5,11 +5,116 @@ using Aspire.Cli.Tests.Utils; using Microsoft.AspNetCore.Certificates.Generation; using Microsoft.Extensions.Logging.Abstractions; +using Microsoft.Extensions.Logging.Testing; namespace Aspire.Cli.Tests.Certificates; public class UnixCertificateManagerTests { + [Fact] + public void GetTrustLevel_WithCorruptOpenSslCertificate_DoesNotThrow() + { + Assert.SkipUnless(OperatingSystem.IsLinux(), "OpenSSL certificate directory trust is only exercised on Linux."); + + var openSslDirectory = Directory.CreateTempSubdirectory(); + + try + { + var environment = TestEnvironment.CreateLinux(new Dictionary + { + ["PATH"] = Path.Combine(openSslDirectory.FullName, "missing-tools"), + ["SSL_CERT_DIR"] = openSslDirectory.FullName, + ["DOTNET_DEV_CERTS_NSSDB_PATHS"] = Path.Combine(openSslDirectory.FullName, "missing-nss-db") + }); + var manager = new UnixCertificateManager(NullLogger.Instance, environment); + using var certificate = manager.CreateAspNetCoreHttpsDevelopmentCertificate( + DateTimeOffset.UtcNow.AddDays(-1), + DateTimeOffset.UtcNow.AddDays(365)); + var certificatePath = Path.Combine(openSslDirectory.FullName, $"aspnetcore-localhost-{certificate.Thumbprint}.pem"); + File.WriteAllText(certificatePath, "not a certificate"); + + var trustLevel = manager.GetTrustLevel(certificate); + + Assert.Equal(CertificateManager.TrustLevel.None, trustLevel); + } + finally + { + openSslDirectory.Delete(recursive: true); + } + } + + [Fact] + public void GetTrustLevel_WithCorruptOpenSslCertificateBeforeValidCertificate_DoesNotLogOpenSslWarning() + { + Assert.SkipUnless(OperatingSystem.IsLinux(), "OpenSSL certificate directory trust is only exercised on Linux."); + + var corruptOpenSslDirectory = Directory.CreateTempSubdirectory(); + var validOpenSslDirectory = Directory.CreateTempSubdirectory(); + + try + { + var sink = new TestSink(); + var logger = new TestLogger(nameof(UnixCertificateManager), sink, enabled: true); + var environment = TestEnvironment.CreateLinux(new Dictionary + { + ["PATH"] = Path.Combine(corruptOpenSslDirectory.FullName, "missing-tools"), + ["SSL_CERT_DIR"] = string.Join(Path.PathSeparator, corruptOpenSslDirectory.FullName, validOpenSslDirectory.FullName), + ["DOTNET_DEV_CERTS_NSSDB_PATHS"] = Path.Combine(corruptOpenSslDirectory.FullName, "missing-nss-db") + }); + var manager = new UnixCertificateManager(logger, environment); + using var certificate = manager.CreateAspNetCoreHttpsDevelopmentCertificate( + DateTimeOffset.UtcNow.AddDays(-1), + DateTimeOffset.UtcNow.AddDays(365)); + var certificateFileName = $"aspnetcore-localhost-{certificate.Thumbprint}.pem"; + File.WriteAllText(Path.Combine(corruptOpenSslDirectory.FullName, certificateFileName), "not a certificate"); + File.WriteAllText(Path.Combine(validOpenSslDirectory.FullName, certificateFileName), certificate.ExportCertificatePem()); + + var trustLevel = manager.GetTrustLevel(certificate); + + Assert.NotEqual(CertificateManager.TrustLevel.None, trustLevel); + Assert.DoesNotContain(sink.Writes, w => w.Message?.Contains("not trusted by OpenSSL", StringComparison.Ordinal) == true); + } + finally + { + corruptOpenSslDirectory.Delete(recursive: true); + validOpenSslDirectory.Delete(recursive: true); + } + } + + [Fact] + public void RemoveCertificate_WithMissingOpenSsl_DeletesOpenSslCertificate() + { + Assert.SkipUnless(OperatingSystem.IsLinux(), "OpenSSL certificate cleanup is only exercised on Linux."); + + var openSslDirectory = Directory.CreateTempSubdirectory(); + + try + { + var environment = TestEnvironment.CreateLinux(new Dictionary + { + ["PATH"] = Path.Combine(openSslDirectory.FullName, "missing-tools"), + ["DOTNET_DEV_CERTS_NSSDB_PATHS"] = Path.Combine(openSslDirectory.FullName, "missing-nss-db"), + [CertificateHelpers.DevCertsOpenSslCertDirEnvVar] = openSslDirectory.FullName + }); + var manager = new UnixCertificateManager(NullLogger.Instance, environment); + using var certificate = manager.CreateAspNetCoreHttpsDevelopmentCertificate( + DateTimeOffset.UtcNow.AddDays(-1), + DateTimeOffset.UtcNow.AddDays(365)); + using var savedCertificate = manager.SaveCertificate(certificate); + var certificatePath = Path.Combine(openSslDirectory.FullName, $"aspnetcore-localhost-{savedCertificate.Thumbprint}.pem"); + File.WriteAllText(certificatePath, "not a certificate"); + + var exception = Record.Exception(() => manager.RemoveCertificate(savedCertificate, CertificateManager.RemoveLocations.All)); + + Assert.Null(exception); + Assert.False(File.Exists(certificatePath)); + } + finally + { + openSslDirectory.Delete(recursive: true); + } + } + [Fact] public void RemoveCertificate_WithMissingCertUtilAndNssDbs_SkipsNssCleanup() { diff --git a/tests/Aspire.Cli.Tests/TestServices/TestProcessExecutionFactory.cs b/tests/Aspire.Cli.Tests/TestServices/TestProcessExecutionFactory.cs index 46ec93256c9..bc0937c4d13 100644 --- a/tests/Aspire.Cli.Tests/TestServices/TestProcessExecutionFactory.cs +++ b/tests/Aspire.Cli.Tests/TestServices/TestProcessExecutionFactory.cs @@ -191,6 +191,8 @@ public bool HasExited public bool StartReturnValue { get; init; } = true; + public Exception? StartException { get; init; } + public bool ThrowOnHasExitedBeforeStart { get; init; } public Func>? WaitForExitAsyncCallback { get; init; } @@ -209,6 +211,11 @@ public Task StartAsync(CancellationToken cancellationToken) { cancellationToken.ThrowIfCancellationRequested(); + if (StartException is not null) + { + throw StartException; + } + if (!StartReturnValue) { return Task.FromResult(false); diff --git a/tests/Aspire.Cli.Tests/Utils/DevCertsCheckTests.cs b/tests/Aspire.Cli.Tests/Utils/DevCertsCheckTests.cs index 8c62d95132d..0e2587b4bbd 100644 --- a/tests/Aspire.Cli.Tests/Utils/DevCertsCheckTests.cs +++ b/tests/Aspire.Cli.Tests/Utils/DevCertsCheckTests.cs @@ -1,6 +1,8 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Security.Cryptography; +using System.Security.Cryptography.X509Certificates; using Aspire.Cli.Certificates; using Aspire.Cli.Tests.TestServices; using Aspire.Cli.Utils.EnvironmentChecker; @@ -29,6 +31,92 @@ private static DevCertInfo CreateDevCertInfo(CertificateManager.TrustLevel trust }; } + private static DevCertInfo CreateDevCertInfo(CertificateManager.TrustLevel trustLevel, X509Certificate2 certificate, int version) + { + return new DevCertInfo + { + TrustLevel = trustLevel, + Thumbprint = certificate.Thumbprint, + Version = version, + ValidityNotBefore = certificate.NotBefore, + ValidityNotAfter = certificate.NotAfter, + Subject = certificate.Subject, + IsHttpsDevelopmentCertificate = true, + IsExportable = true + }; + } + + private static X509Certificate2 CreateCertificate() + { + using var key = RSA.Create(2048); + var request = new CertificateRequest("CN=localhost", key, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1); + using var certificate = request.CreateSelfSigned(DateTimeOffset.UtcNow.AddDays(-1), DateTimeOffset.UtcNow.AddDays(1)); + return X509CertificateLoader.LoadCertificate(certificate.Export(X509ContentType.Cert)); + } + + private static DevCertsCheck CreateCheck(TestCertificateToolRunner toolRunner, IEnvironment environment, TestProcessExecutionFactory? processExecutionFactory = null) => + new(NullLogger.Instance, toolRunner, environment, processExecutionFactory ?? CreateOpenSslProcessExecutionFactory()); + + private static TestProcessExecutionFactory CreateOpenSslProcessExecutionFactory(string hash = "12345678") => + new() + { + AsyncAttemptCallback = (_, _, _) => Task.FromResult((0, (string?)hash)) + }; + + private static string CreateCertUtil(DirectoryInfo directory) + { + var certUtilPath = Path.Combine(directory.FullName, CertificateHelpers.CertUtilCommand); + File.WriteAllText(certUtilPath, ""); + if (!OperatingSystem.IsWindows()) + { + File.SetUnixFileMode(certUtilPath, UnixFileMode.UserRead | UnixFileMode.UserExecute); + } + + return certUtilPath; + } + + private static string CreateOpenSsl(DirectoryInfo directory, string hash) + { + var openSslPath = OperatingSystem.IsWindows() + ? Path.Combine(directory.FullName, "openssl.cmd") + : Path.Combine(directory.FullName, "openssl"); + + var contents = OperatingSystem.IsWindows() + ? $""" + @echo off + if "%1"=="x509" ( + echo {hash} + exit /b 0 + ) + exit /b 1 + """ + : $""" + #!/bin/sh + if [ "$1" = "x509" ]; then + echo {hash} + exit 0 + fi + exit 1 + """; + File.WriteAllText(openSslPath, contents); + if (!OperatingSystem.IsWindows()) + { + File.SetUnixFileMode(openSslPath, UnixFileMode.UserRead | UnixFileMode.UserExecute); + } + + return openSslPath; + } + + private static string GetOpenSslCertificateFileName(X509Certificate2 certificate) => + $"aspnetcore-localhost-{certificate.Thumbprint}.pem"; + + private static void WriteOpenSslCertificateCache(DirectoryInfo trustDirectory, X509Certificate2 certificate, string hashEntryName = "12345678.0") + { + var certificatePem = certificate.ExportCertificatePem(); + File.WriteAllText(Path.Combine(trustDirectory.FullName, GetOpenSslCertificateFileName(certificate)), certificatePem); + File.WriteAllText(Path.Combine(trustDirectory.FullName, hashEntryName), certificatePem); + } + [Fact] public void EvaluateCertificateResults_NoCertificates_ReturnsWarning() { @@ -254,7 +342,7 @@ public async Task CheckAsync_LinuxWithoutCertUtil_ReturnsCertUtilWarning() { ["PATH"] = Path.Combine(AppContext.BaseDirectory, Guid.NewGuid().ToString("N")) }); - var check = new DevCertsCheck(NullLogger.Instance, toolRunner, environment); + var check = CreateCheck(toolRunner, environment); var results = await check.CheckAsync(); @@ -270,12 +358,7 @@ public async Task CheckAsync_LinuxWithCertUtil_DoesNotReturnCertUtilWarning() try { - var certUtilPath = Path.Combine(tempDirectory.FullName, CertificateHelpers.CertUtilCommand); - File.WriteAllText(certUtilPath, ""); - if (!OperatingSystem.IsWindows()) - { - File.SetUnixFileMode(certUtilPath, UnixFileMode.UserRead | UnixFileMode.UserExecute); - } + CreateCertUtil(tempDirectory); var certs = new List { @@ -294,7 +377,7 @@ public async Task CheckAsync_LinuxWithCertUtil_DoesNotReturnCertUtilWarning() { ["PATH"] = tempDirectory.FullName }); - var check = new DevCertsCheck(NullLogger.Instance, toolRunner, environment); + var check = CreateCheck(toolRunner, environment); var results = await check.CheckAsync(); @@ -306,6 +389,638 @@ public async Task CheckAsync_LinuxWithCertUtil_DoesNotReturnCertUtilWarning() } } + [Fact] + public async Task CheckAsync_LinuxWithMatchingOpenSslCertificateCache_DoesNotReturnOpenSslCertificateCacheWarning() + { + using var certificate = CreateCertificate(); + var tempDirectory = Directory.CreateTempSubdirectory(); + + try + { + CreateCertUtil(tempDirectory); + if (!OperatingSystem.IsWindows()) + { + CreateOpenSsl(tempDirectory, "12345678"); + } + + var trustDirectory = Directory.CreateDirectory(Path.Combine(tempDirectory.FullName, "trust")); + WriteOpenSslCertificateCache(trustDirectory, certificate); + + var certs = new List + { + CreateDevCertInfo(CertificateManager.TrustLevel.Full, certificate, MinVersion), + }; + var toolRunner = new TestCertificateToolRunner + { + CheckHttpCertificateCallback = () => new CertificateTrustResult + { + HasCertificates = true, + TrustLevel = CertificateManager.TrustLevel.Full, + Certificates = certs + } + }; + var environment = TestEnvironment.CreateLinux(new Dictionary + { + ["PATH"] = tempDirectory.FullName, + [CertificateHelpers.DevCertsOpenSslCertDirEnvVar] = trustDirectory.FullName + }); + var check = CreateCheck(toolRunner, environment); + + var results = await check.CheckAsync(); + + Assert.DoesNotContain(results, r => r.Name == DevCertsCheck.OpenSslCertificateCacheCheckName); + } + finally + { + tempDirectory.Delete(recursive: true); + } + } + + [Fact] + [SkipOnPlatform(TestPlatforms.Windows, "The synthetic openssl command uses a POSIX shell script.")] + public async Task CheckAsync_LinuxWithFailedOpenSslHashProbeAndMatchingHashStyleEntry_ReturnsOpenSslCertificateCacheWarning() + { + using var certificate = CreateCertificate(); + var tempDirectory = Directory.CreateTempSubdirectory(); + + try + { + CreateCertUtil(tempDirectory); + CreateOpenSsl(tempDirectory, "12345678"); + var trustDirectory = Directory.CreateDirectory(Path.Combine(tempDirectory.FullName, "trust")); + WriteOpenSslCertificateCache(trustDirectory, certificate, "87654321.0"); + + var certs = new List + { + CreateDevCertInfo(CertificateManager.TrustLevel.Full, certificate, MinVersion), + }; + var toolRunner = new TestCertificateToolRunner + { + CheckHttpCertificateCallback = () => new CertificateTrustResult + { + HasCertificates = true, + TrustLevel = CertificateManager.TrustLevel.Full, + Certificates = certs + } + }; + var environment = TestEnvironment.CreateLinux(new Dictionary + { + ["PATH"] = tempDirectory.FullName, + [CertificateHelpers.DevCertsOpenSslCertDirEnvVar] = trustDirectory.FullName + }); + var processExecutionFactory = new TestProcessExecutionFactory + { + AsyncAttemptCallback = (_, _, _) => Task.FromResult((1, (string?)null)) + }; + var check = CreateCheck(toolRunner, environment, processExecutionFactory); + + var results = await check.CheckAsync(); + + var cacheResult = Assert.Single(results, r => r.Name == DevCertsCheck.OpenSslCertificateCacheCheckName); + Assert.Equal(EnvironmentCheckStatus.Warning, cacheResult.Status); + Assert.Contains(certificate.Thumbprint, cacheResult.Details); + Assert.Contains("subject-hash", cacheResult.Details); + } + finally + { + tempDirectory.Delete(recursive: true); + } + } + + [Fact] + [SkipOnPlatform(TestPlatforms.Windows, "The synthetic openssl command uses a POSIX shell script.")] + public async Task CheckAsync_LinuxWithOpenSslHashProbeStartFailure_ReturnsOpenSslCertificateCacheWarning() + { + using var certificate = CreateCertificate(); + var tempDirectory = Directory.CreateTempSubdirectory(); + + try + { + CreateCertUtil(tempDirectory); + CreateOpenSsl(tempDirectory, "12345678"); + var trustDirectory = Directory.CreateDirectory(Path.Combine(tempDirectory.FullName, "trust")); + WriteOpenSslCertificateCache(trustDirectory, certificate, "12345678.0"); + + var certs = new List + { + CreateDevCertInfo(CertificateManager.TrustLevel.Full, certificate, MinVersion), + }; + var toolRunner = new TestCertificateToolRunner + { + CheckHttpCertificateCallback = () => new CertificateTrustResult + { + HasCertificates = true, + TrustLevel = CertificateManager.TrustLevel.Full, + Certificates = certs + } + }; + var environment = TestEnvironment.CreateLinux(new Dictionary + { + ["PATH"] = tempDirectory.FullName, + [CertificateHelpers.DevCertsOpenSslCertDirEnvVar] = trustDirectory.FullName + }); + var processExecutionFactory = new TestProcessExecutionFactory + { + CreateExecutionWithFileNameCallback = (fileName, args, env, workingDirectory, options) => + new TestProcessExecution(fileName, args, env, options, (_, _, _) => Task.FromResult((0, (string?)"12345678")), () => 1) + { + StartException = new InvalidOperationException("openssl failed to start") + } + }; + var check = CreateCheck(toolRunner, environment, processExecutionFactory); + + var results = await check.CheckAsync(); + + var cacheResult = Assert.Single(results, r => r.Name == DevCertsCheck.OpenSslCertificateCacheCheckName); + Assert.Equal(EnvironmentCheckStatus.Warning, cacheResult.Status); + Assert.Contains(certificate.Thumbprint, cacheResult.Details); + Assert.Contains("subject-hash", cacheResult.Details); + } + finally + { + tempDirectory.Delete(recursive: true); + } + } + + [Fact] + [SkipOnPlatform(TestPlatforms.Windows, "The synthetic openssl command uses a POSIX shell script.")] + public async Task CheckAsync_LinuxWithMissingOpenSslHashEntry_ReturnsOpenSslCertificateCacheWarning() + { + using var certificate = CreateCertificate(); + var tempDirectory = Directory.CreateTempSubdirectory(); + + try + { + CreateCertUtil(tempDirectory); + CreateOpenSsl(tempDirectory, "12345678"); + var trustDirectory = Directory.CreateDirectory(Path.Combine(tempDirectory.FullName, "trust")); + File.WriteAllText(Path.Combine(trustDirectory.FullName, GetOpenSslCertificateFileName(certificate)), certificate.ExportCertificatePem()); + + var certs = new List + { + CreateDevCertInfo(CertificateManager.TrustLevel.Full, certificate, MinVersion), + }; + var toolRunner = new TestCertificateToolRunner + { + CheckHttpCertificateCallback = () => new CertificateTrustResult + { + HasCertificates = true, + TrustLevel = CertificateManager.TrustLevel.Full, + Certificates = certs + } + }; + var environment = TestEnvironment.CreateLinux(new Dictionary + { + ["PATH"] = tempDirectory.FullName, + [CertificateHelpers.DevCertsOpenSslCertDirEnvVar] = trustDirectory.FullName + }); + var check = CreateCheck(toolRunner, environment); + + var results = await check.CheckAsync(); + + var cacheResult = Assert.Single(results, r => r.Name == DevCertsCheck.OpenSslCertificateCacheCheckName); + Assert.Equal(EnvironmentCheckStatus.Warning, cacheResult.Status); + Assert.Contains(certificate.Thumbprint, cacheResult.Details); + Assert.Contains("subject-hash", cacheResult.Details); + Assert.Contains("aspire certs clean", cacheResult.Fix); + Assert.DoesNotContain("Install openssl", cacheResult.Fix); + } + finally + { + tempDirectory.Delete(recursive: true); + } + } + + [Fact] + [SkipOnPlatform(TestPlatforms.Windows, "The synthetic openssl command uses a POSIX shell script.")] + public async Task CheckAsync_LinuxWithCallerCanceledOpenSslHashProbeAndKillFailure_ThrowsOperationCanceledException() + { + using var certificate = CreateCertificate(); + var tempDirectory = Directory.CreateTempSubdirectory(); + using var cancellationTokenSource = new CancellationTokenSource(); + + try + { + CreateCertUtil(tempDirectory); + CreateOpenSsl(tempDirectory, "12345678"); + var trustDirectory = Directory.CreateDirectory(Path.Combine(tempDirectory.FullName, "trust")); + File.WriteAllText(Path.Combine(trustDirectory.FullName, GetOpenSslCertificateFileName(certificate)), certificate.ExportCertificatePem()); + + var certs = new List + { + CreateDevCertInfo(CertificateManager.TrustLevel.Full, certificate, MinVersion), + }; + var toolRunner = new TestCertificateToolRunner + { + CheckHttpCertificateCallback = () => new CertificateTrustResult + { + HasCertificates = true, + TrustLevel = CertificateManager.TrustLevel.Full, + Certificates = certs + } + }; + var environment = TestEnvironment.CreateLinux(new Dictionary + { + ["PATH"] = tempDirectory.FullName, + [CertificateHelpers.DevCertsOpenSslCertDirEnvVar] = trustDirectory.FullName + }); + var processExecutionFactory = new TestProcessExecutionFactory + { + CreateExecutionWithFileNameCallback = (fileName, args, env, workingDirectory, options) => + new TestProcessExecution(fileName, args, env, options, async (_, _, cancellationToken) => + { + await cancellationTokenSource.CancelAsync(); + await Task.Delay(TimeSpan.FromSeconds(30), cancellationToken); + return (0, (string?)"12345678"); + }, () => 1) + { + KillCallback = _ => throw new NotSupportedException("Process tree termination is unavailable.") + } + }; + var check = CreateCheck(toolRunner, environment, processExecutionFactory); + + await Assert.ThrowsAnyAsync(() => check.CheckAsync(cancellationTokenSource.Token)); + + var processExecution = Assert.IsType(Assert.Single(processExecutionFactory.CreatedExecutions)); + Assert.Equal(1, processExecution.KillCount); + Assert.True(processExecution.KilledEntireProcessTree); + } + finally + { + tempDirectory.Delete(recursive: true); + } + } + + [Fact] + [SkipOnPlatform(TestPlatforms.Windows, "The synthetic openssl command uses a POSIX shell script.")] + public async Task CheckAsync_LinuxWithOpenSslHashProbeFailureAndKillFailure_ReturnsOpenSslCertificateCacheWarning() + { + using var certificate = CreateCertificate(); + var tempDirectory = Directory.CreateTempSubdirectory(); + + try + { + CreateCertUtil(tempDirectory); + CreateOpenSsl(tempDirectory, "12345678"); + var trustDirectory = Directory.CreateDirectory(Path.Combine(tempDirectory.FullName, "trust")); + File.WriteAllText(Path.Combine(trustDirectory.FullName, GetOpenSslCertificateFileName(certificate)), certificate.ExportCertificatePem()); + + var certs = new List + { + CreateDevCertInfo(CertificateManager.TrustLevel.Full, certificate, MinVersion), + }; + var toolRunner = new TestCertificateToolRunner + { + CheckHttpCertificateCallback = () => new CertificateTrustResult + { + HasCertificates = true, + TrustLevel = CertificateManager.TrustLevel.Full, + Certificates = certs + } + }; + var environment = TestEnvironment.CreateLinux(new Dictionary + { + ["PATH"] = tempDirectory.FullName, + [CertificateHelpers.DevCertsOpenSslCertDirEnvVar] = trustDirectory.FullName + }); + var processExecutionFactory = new TestProcessExecutionFactory + { + CreateExecutionWithFileNameCallback = (fileName, args, env, workingDirectory, options) => + new TestProcessExecution(fileName, args, env, options, (_, _, _) => throw new IOException("openssl pipe failed"), () => 1) + { + KillCallback = _ => throw new NotSupportedException("Process tree termination is unavailable.") + } + }; + var check = CreateCheck(toolRunner, environment, processExecutionFactory); + + var results = await check.CheckAsync(); + + var cacheResult = Assert.Single(results, r => r.Name == DevCertsCheck.OpenSslCertificateCacheCheckName); + Assert.Equal(EnvironmentCheckStatus.Warning, cacheResult.Status); + Assert.Contains(certificate.Thumbprint, cacheResult.Details); + Assert.Contains("subject-hash", cacheResult.Details); + + var processExecution = Assert.IsType(Assert.Single(processExecutionFactory.CreatedExecutions)); + Assert.Equal(1, processExecution.KillCount); + Assert.True(processExecution.KilledEntireProcessTree); + } + finally + { + tempDirectory.Delete(recursive: true); + } + } + + [Fact] + public async Task CheckAsync_LinuxWithMissingOpenSslHashEntryAndNoOpenSsl_DoesNotReturnOpenSslCertificateCacheWarning() + { + using var certificate = CreateCertificate(); + var tempDirectory = Directory.CreateTempSubdirectory(); + + try + { + CreateCertUtil(tempDirectory); + var trustDirectory = Directory.CreateDirectory(Path.Combine(tempDirectory.FullName, "trust")); + File.WriteAllText(Path.Combine(trustDirectory.FullName, GetOpenSslCertificateFileName(certificate)), certificate.ExportCertificatePem()); + + var certs = new List + { + CreateDevCertInfo(CertificateManager.TrustLevel.Full, certificate, MinVersion), + }; + var toolRunner = new TestCertificateToolRunner + { + CheckHttpCertificateCallback = () => new CertificateTrustResult + { + HasCertificates = true, + TrustLevel = CertificateManager.TrustLevel.Full, + Certificates = certs + } + }; + var environment = TestEnvironment.CreateLinux(new Dictionary + { + ["PATH"] = tempDirectory.FullName, + [CertificateHelpers.DevCertsOpenSslCertDirEnvVar] = trustDirectory.FullName + }); + var check = CreateCheck(toolRunner, environment); + + var results = await check.CheckAsync(); + + Assert.DoesNotContain(results, r => r.Name == DevCertsCheck.OpenSslCertificateCacheCheckName); + } + finally + { + tempDirectory.Delete(recursive: true); + } + } + + [Fact] + public async Task CheckAsync_LinuxWithMissingOpenSslCertificateCacheDirectory_ReturnsOpenSslCertificateCacheWarning() + { + using var certificate = CreateCertificate(); + var tempDirectory = Directory.CreateTempSubdirectory(); + + try + { + CreateCertUtil(tempDirectory); + var trustDirectory = Path.Combine(tempDirectory.FullName, "missing-trust"); + + var certs = new List + { + CreateDevCertInfo(CertificateManager.TrustLevel.Full, certificate, MinVersion), + }; + var toolRunner = new TestCertificateToolRunner + { + CheckHttpCertificateCallback = () => new CertificateTrustResult + { + HasCertificates = true, + TrustLevel = CertificateManager.TrustLevel.Full, + Certificates = certs + } + }; + var environment = TestEnvironment.CreateLinux(new Dictionary + { + ["PATH"] = tempDirectory.FullName, + [CertificateHelpers.DevCertsOpenSslCertDirEnvVar] = trustDirectory + }); + var check = CreateCheck(toolRunner, environment); + + var results = await check.CheckAsync(); + + var cacheResult = Assert.Single(results, r => r.Name == DevCertsCheck.OpenSslCertificateCacheCheckName); + Assert.Equal(EnvironmentCheckStatus.Warning, cacheResult.Status); + Assert.Contains(certificate.Thumbprint, cacheResult.Details); + Assert.Contains("aspire certs clean", cacheResult.Fix); + } + finally + { + tempDirectory.Delete(recursive: true); + } + } + + [Fact] + public async Task CheckAsync_LinuxWithMissingOpenSslCertificateCacheEntry_ReturnsOpenSslCertificateCacheWarning() + { + using var certificate = CreateCertificate(); + var tempDirectory = Directory.CreateTempSubdirectory(); + + try + { + CreateCertUtil(tempDirectory); + var trustDirectory = Directory.CreateDirectory(Path.Combine(tempDirectory.FullName, "trust")); + + var certs = new List + { + CreateDevCertInfo(CertificateManager.TrustLevel.Full, certificate, MinVersion), + }; + var toolRunner = new TestCertificateToolRunner + { + CheckHttpCertificateCallback = () => new CertificateTrustResult + { + HasCertificates = true, + TrustLevel = CertificateManager.TrustLevel.Full, + Certificates = certs + } + }; + var environment = TestEnvironment.CreateLinux(new Dictionary + { + ["PATH"] = tempDirectory.FullName, + [CertificateHelpers.DevCertsOpenSslCertDirEnvVar] = trustDirectory.FullName + }); + var check = CreateCheck(toolRunner, environment); + + var results = await check.CheckAsync(); + + var cacheResult = Assert.Single(results, r => r.Name == DevCertsCheck.OpenSslCertificateCacheCheckName); + Assert.Equal(EnvironmentCheckStatus.Warning, cacheResult.Status); + Assert.Contains(certificate.Thumbprint, cacheResult.Details); + Assert.Contains("aspire certs clean", cacheResult.Fix); + } + finally + { + tempDirectory.Delete(recursive: true); + } + } + + [Fact] + public async Task CheckAsync_LinuxWithCorruptOpenSslCertificateCache_ReturnsOpenSslCertificateCacheWarning() + { + using var certificate = CreateCertificate(); + var tempDirectory = Directory.CreateTempSubdirectory(); + + try + { + CreateCertUtil(tempDirectory); + var trustDirectory = Directory.CreateDirectory(Path.Combine(tempDirectory.FullName, "trust")); + var corruptCertificateFileName = GetOpenSslCertificateFileName(certificate); + File.WriteAllText(Path.Combine(trustDirectory.FullName, corruptCertificateFileName), "not a certificate"); + + var certs = new List + { + CreateDevCertInfo(CertificateManager.TrustLevel.Full, certificate, MinVersion), + }; + var toolRunner = new TestCertificateToolRunner + { + CheckHttpCertificateCallback = () => new CertificateTrustResult + { + HasCertificates = true, + TrustLevel = CertificateManager.TrustLevel.Full, + Certificates = certs + } + }; + var environment = TestEnvironment.CreateLinux(new Dictionary + { + ["PATH"] = tempDirectory.FullName, + [CertificateHelpers.DevCertsOpenSslCertDirEnvVar] = trustDirectory.FullName + }); + var check = CreateCheck(toolRunner, environment); + + var results = await check.CheckAsync(); + + var cacheResult = Assert.Single(results, r => r.Name == DevCertsCheck.OpenSslCertificateCacheCheckName); + Assert.Equal(EnvironmentCheckStatus.Warning, cacheResult.Status); + Assert.Contains(corruptCertificateFileName, cacheResult.Details); + Assert.Contains("aspire certs clean", cacheResult.Fix); + } + finally + { + tempDirectory.Delete(recursive: true); + } + } + + [Fact] + public async Task CheckAsync_LinuxWithUntrustedCertificateAndCorruptOpenSslCertificateCache_ReturnsOpenSslCertificateCacheWarning() + { + using var certificate = CreateCertificate(); + var tempDirectory = Directory.CreateTempSubdirectory(); + + try + { + CreateCertUtil(tempDirectory); + var trustDirectory = Directory.CreateDirectory(Path.Combine(tempDirectory.FullName, "trust")); + var corruptCertificateFileName = GetOpenSslCertificateFileName(certificate); + File.WriteAllText(Path.Combine(trustDirectory.FullName, corruptCertificateFileName), "not a certificate"); + + var certs = new List + { + CreateDevCertInfo(CertificateManager.TrustLevel.None, certificate, MinVersion), + }; + var toolRunner = new TestCertificateToolRunner + { + CheckHttpCertificateCallback = () => new CertificateTrustResult + { + HasCertificates = true, + TrustLevel = CertificateManager.TrustLevel.None, + Certificates = certs + } + }; + var environment = TestEnvironment.CreateLinux(new Dictionary + { + ["PATH"] = tempDirectory.FullName, + [CertificateHelpers.DevCertsOpenSslCertDirEnvVar] = trustDirectory.FullName + }); + var check = CreateCheck(toolRunner, environment); + + var results = await check.CheckAsync(); + + var cacheResult = Assert.Single(results, r => r.Name == DevCertsCheck.OpenSslCertificateCacheCheckName); + Assert.Equal(EnvironmentCheckStatus.Warning, cacheResult.Status); + Assert.Contains(corruptCertificateFileName, cacheResult.Details); + Assert.Contains("aspire certs clean", cacheResult.Fix); + } + finally + { + tempDirectory.Delete(recursive: true); + } + } + + [Fact] + public async Task CheckAsync_LinuxWithUnrelatedCorruptOpenSslCertificateCache_DoesNotReturnOpenSslCertificateCacheWarning() + { + using var certificate = CreateCertificate(); + var tempDirectory = Directory.CreateTempSubdirectory(); + + try + { + CreateCertUtil(tempDirectory); + var trustDirectory = Directory.CreateDirectory(Path.Combine(tempDirectory.FullName, "trust")); + WriteOpenSslCertificateCache(trustDirectory, certificate); + File.WriteAllText(Path.Combine(trustDirectory.FullName, "unrelated.pem"), "not a certificate"); + + var certs = new List + { + CreateDevCertInfo(CertificateManager.TrustLevel.Full, certificate, MinVersion), + }; + var toolRunner = new TestCertificateToolRunner + { + CheckHttpCertificateCallback = () => new CertificateTrustResult + { + HasCertificates = true, + TrustLevel = CertificateManager.TrustLevel.Full, + Certificates = certs + } + }; + var environment = TestEnvironment.CreateLinux(new Dictionary + { + ["PATH"] = tempDirectory.FullName, + [CertificateHelpers.DevCertsOpenSslCertDirEnvVar] = trustDirectory.FullName + }); + var check = CreateCheck(toolRunner, environment); + + var results = await check.CheckAsync(); + + Assert.DoesNotContain(results, r => r.Name == DevCertsCheck.OpenSslCertificateCacheCheckName); + } + finally + { + tempDirectory.Delete(recursive: true); + } + } + + [Fact] + public async Task CheckAsync_LinuxWithStaleOpenSslCertificateCache_ReturnsOpenSslCertificateCacheWarning() + { + using var certificate = CreateCertificate(); + using var staleCertificate = CreateCertificate(); + var tempDirectory = Directory.CreateTempSubdirectory(); + + try + { + CreateCertUtil(tempDirectory); + var trustDirectory = Directory.CreateDirectory(Path.Combine(tempDirectory.FullName, "trust")); + File.WriteAllText(Path.Combine(trustDirectory.FullName, GetOpenSslCertificateFileName(certificate)), staleCertificate.ExportCertificatePem()); + + var certs = new List + { + CreateDevCertInfo(CertificateManager.TrustLevel.Full, certificate, MinVersion), + }; + var toolRunner = new TestCertificateToolRunner + { + CheckHttpCertificateCallback = () => new CertificateTrustResult + { + HasCertificates = true, + TrustLevel = CertificateManager.TrustLevel.Full, + Certificates = certs + } + }; + var environment = TestEnvironment.CreateLinux(new Dictionary + { + ["PATH"] = tempDirectory.FullName, + [CertificateHelpers.DevCertsOpenSslCertDirEnvVar] = trustDirectory.FullName + }); + var check = CreateCheck(toolRunner, environment); + + var results = await check.CheckAsync(); + + var cacheResult = Assert.Single(results, r => r.Name == DevCertsCheck.OpenSslCertificateCacheCheckName); + Assert.Equal(EnvironmentCheckStatus.Warning, cacheResult.Status); + Assert.Contains(certificate.Thumbprint, cacheResult.Details); + Assert.Contains("aspire certs clean", cacheResult.Fix); + } + finally + { + tempDirectory.Delete(recursive: true); + } + } + [Fact] public async Task CheckAsync_NonLinux_DoesNotReadEnvironmentVariablesForCertUtilWarning() { @@ -326,7 +1041,7 @@ public async Task CheckAsync_NonLinux_DoesNotReadEnvironmentVariablesForCertUtil { ["PATH"] = Path.Combine(AppContext.BaseDirectory, Guid.NewGuid().ToString("N")) }); - var check = new DevCertsCheck(NullLogger.Instance, toolRunner, environment); + var check = CreateCheck(toolRunner, environment); var results = await check.CheckAsync(); diff --git a/tests/Aspire.Hosting.Tests/Dcp/DcpExecutorTests.cs b/tests/Aspire.Hosting.Tests/Dcp/DcpExecutorTests.cs index a2b8b787161..384fe9fa7f9 100644 --- a/tests/Aspire.Hosting.Tests/Dcp/DcpExecutorTests.cs +++ b/tests/Aspire.Hosting.Tests/Dcp/DcpExecutorTests.cs @@ -25,6 +25,7 @@ using Aspire.Hosting.UserSecrets; using k8s.Models; using Microsoft.AspNetCore.InternalTesting; +using Microsoft.DotNet.RemoteExecutor; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.FileProviders; @@ -3541,6 +3542,118 @@ public async Task PersistentPlainExecutable_UsesStableCertificateOutputPath() Assert.Equal(Path.Join(expectedCertificatesRoot, "cert.pem"), sslCertFile); } + [Fact] + public void PlainExecutableCertificateDirectoriesPath_IncludesExistingWellKnownDirectoriesForAppendWhenSslCertDirIsUnsetOnLinux() + { + Assert.SkipUnless(OperatingSystem.IsLinux(), "OpenSSL default certificate directories are only inferred on Linux."); + + var options = new RemoteInvokeOptions(); + options.StartInfo.Environment.Remove("SSL_CERT_DIR"); + + RemoteExecutor.Invoke(static async () => + { + Environment.SetEnvironmentVariable("SSL_CERT_DIR", null); + + var expectedWellKnownCertificateDirectories = ContainerCertificatePathsAnnotation.DefaultCertificateDirectoriesPaths + .Where(Directory.Exists) + .ToArray(); + Assert.NotEmpty(expectedWellKnownCertificateDirectories); + + var builder = DistributedApplication.CreateBuilder(); + using var certificate = CreateTestCertificate(); + var certificateAuthorities = builder.AddCertificateAuthorityCollection("certificates") + .WithCertificate(certificate); + + var executable = new TestExecutableResource("test-working-directory"); + builder.AddResource(executable) + .WithCertificateAuthorityCollection(certificateAuthorities); + + var kubernetesService = new TestKubernetesService(); + using var app = builder.Build(); + var distributedAppModel = app.Services.GetRequiredService(); + var appExecutor = CreateAppExecutor(distributedAppModel, kubernetesService: kubernetesService); + + await appExecutor.RunApplicationAsync(); + + var exe = Assert.Single(kubernetesService.CreatedResources.OfType(), e => e.AppModelResourceName == "TestExecutable"); + var sslCertDir = Assert.Single(exe.Spec.Env!, e => e.Name == "SSL_CERT_DIR").Value; + + Assert.NotNull(sslCertDir); + var sslCertDirs = sslCertDir.Split(Path.PathSeparator); + Assert.EndsWith($"{Path.DirectorySeparatorChar}certs", sslCertDirs[0]); + Assert.Equal(expectedWellKnownCertificateDirectories, sslCertDirs.Skip(1)); + }, options).Dispose(); + } + + [Fact] + public void PlainExecutableCertificateDirectoriesPath_PreservesAppHostSslCertDirForAppend() + { + var expectedExistingSslCertDirs = new[] { "/custom/certs", "/home/me/.aspnet/dev-certs/trust" }; + var options = new RemoteInvokeOptions(); + options.StartInfo.Environment["SSL_CERT_DIR"] = string.Join(Path.PathSeparator, expectedExistingSslCertDirs); + + RemoteExecutor.Invoke(static expectedExistingSslCertDir => + { + Environment.SetEnvironmentVariable("SSL_CERT_DIR", expectedExistingSslCertDir); + + var sslCertDir = GetPlainExecutableSslCertDirAsync().GetAwaiter().GetResult(); + + Assert.NotNull(sslCertDir); + var sslCertDirs = sslCertDir.Split(Path.PathSeparator); + Assert.EndsWith($"{Path.DirectorySeparatorChar}certs", sslCertDirs[0]); + Assert.Equal(expectedExistingSslCertDir.Split(Path.PathSeparator), sslCertDirs.Skip(1)); + }, string.Join(Path.PathSeparator, expectedExistingSslCertDirs), options).Dispose(); + } + + [Fact] + public void PlainExecutableCertificateDirectoriesPath_PreservesEmptyAppHostSslCertDirForAppend() + { + var options = new RemoteInvokeOptions(); + options.StartInfo.Environment["SSL_CERT_DIR"] = string.Empty; + + RemoteExecutor.Invoke(static () => + { + var sslCertDir = GetPlainExecutableSslCertDirAsync().GetAwaiter().GetResult(); + + Assert.NotNull(sslCertDir); + var sslCertDirs = sslCertDir.Split(Path.PathSeparator); + Assert.Single(sslCertDirs); + Assert.EndsWith($"{Path.DirectorySeparatorChar}certs", sslCertDirs[0]); + }, options).Dispose(); + } + + [Fact] + public void PlainExecutableCertificateDirectoriesPath_DoesNotIncludeAppHostSslCertDirForOverride() + { + var options = new RemoteInvokeOptions(); + options.StartInfo.Environment["SSL_CERT_DIR"] = "/custom/certs"; + + RemoteExecutor.Invoke(static () => + { + Environment.SetEnvironmentVariable("SSL_CERT_DIR", "/custom/certs"); + + var sslCertDir = GetPlainExecutableSslCertDirAsync(builder => builder.WithCertificateTrustScope(CertificateTrustScope.Override)).GetAwaiter().GetResult(); + + Assert.NotNull(sslCertDir); + var sslCertDirs = sslCertDir.Split(Path.PathSeparator); + Assert.Single(sslCertDirs); + Assert.EndsWith($"{Path.DirectorySeparatorChar}certs", sslCertDirs[0]); + }, options).Dispose(); + } + + [Fact] + public async Task PlainExecutableCertificateDirectoriesPath_IgnoresResourceSslCertDirForAppend() + { + var customSslCertDir = $"/resource-certs-{Guid.NewGuid():N}"; + + var sslCertDir = await GetPlainExecutableSslCertDirAsync(builder => builder.WithEnvironment("SSL_CERT_DIR", customSslCertDir)); + + Assert.NotNull(sslCertDir); + var sslCertDirs = sslCertDir.Split(Path.PathSeparator); + Assert.EndsWith($"{Path.DirectorySeparatorChar}certs", sslCertDirs[0]); + Assert.All(sslCertDirs, dir => Assert.NotEqual(customSslCertDir, dir)); + } + [Fact] public async Task SessionScopedExplicitStartPlainExecutable_DefersDcpObjectCreationUntilManualStart() { @@ -6713,6 +6826,29 @@ private static DcpExecutor CreateAppExecutor( userSecretsManager ?? NoopUserSecretsManager.Instance); } + private static async Task GetPlainExecutableSslCertDirAsync(Action>? configure = null) + { + var builder = DistributedApplication.CreateBuilder(); + using var certificate = CreateTestCertificate(); + var certificateAuthorities = builder.AddCertificateAuthorityCollection("certificates") + .WithCertificate(certificate); + + var executable = new TestExecutableResource("test-working-directory"); + var executableBuilder = builder.AddResource(executable) + .WithCertificateAuthorityCollection(certificateAuthorities); + configure?.Invoke(executableBuilder); + + var kubernetesService = new TestKubernetesService(); + using var app = builder.Build(); + var distributedAppModel = app.Services.GetRequiredService(); + var appExecutor = CreateAppExecutor(distributedAppModel, kubernetesService: kubernetesService); + + await appExecutor.RunApplicationAsync(); + + var exe = Assert.Single(kubernetesService.CreatedResources.OfType(), e => e.AppModelResourceName == "TestExecutable"); + return Assert.Single(exe.Spec.Env!, e => e.Name == "SSL_CERT_DIR").Value; + } + private static void AssertPortAllocatedFromProxylessEndpointAllocatorRange(int port) { var defaultOptions = new DcpOptions();