diff --git a/src/Aspire.Hosting.NodeJs/NodeExtensions.cs b/src/Aspire.Hosting.NodeJs/NodeExtensions.cs index 4f4c8169724..4bdd94818c8 100644 --- a/src/Aspire.Hosting.NodeJs/NodeExtensions.cs +++ b/src/Aspire.Hosting.NodeJs/NodeExtensions.cs @@ -74,13 +74,14 @@ private static IResourceBuilder WithNodeDefaults(this IResource .WithEnvironment("NODE_ENV", builder.ApplicationBuilder.Environment.IsDevelopment() ? "development" : "production") .WithExecutableCertificateTrustCallback((ctx) => { - if (ctx.Scope == CustomCertificateAuthoritiesScope.Append) + if (ctx.Scope == CertificateTrustScope.Append) { ctx.CertificateBundleEnvironment.Add("NODE_EXTRA_CA_CERTS"); } else { ctx.CertificateTrustArguments.Add("--use-openssl-ca"); + ctx.CertificateBundleEnvironment.Add("SSL_CERT_FILE"); } return Task.CompletedTask; diff --git a/src/Aspire.Hosting.Python/PythonAppResourceBuilderExtensions.cs b/src/Aspire.Hosting.Python/PythonAppResourceBuilderExtensions.cs index c5522fae7ba..55a87f74f6b 100644 --- a/src/Aspire.Hosting.Python/PythonAppResourceBuilderExtensions.cs +++ b/src/Aspire.Hosting.Python/PythonAppResourceBuilderExtensions.cs @@ -3,6 +3,7 @@ using System.ComponentModel; using System.Runtime.CompilerServices; + #pragma warning disable ASPIREEXTENSION001 using Aspire.Hosting.ApplicationModel; using Aspire.Hosting.Python; @@ -281,20 +282,30 @@ private static IResourceBuilder AddPythonAppCore( }); // Configure required environment variables for custom certificate trust when running as an executable - resourceBuilder.WithExecutableCertificateTrustCallback(ctx => - { - if (ctx.Scope == CustomCertificateAuthoritiesScope.Override) + // Python defaults to using System scope to allow combining custom CAs with system CAs as there's no clean + // way to simply append additional certificates to default Python trust stores such as certifi. + resourceBuilder + .WithCertificateTrustScope(CertificateTrustScope.System) + .WithExecutableCertificateTrustCallback(ctx => { - // See: https://docs.python-requests.org/en/latest/user/advanced/#ssl-cert-verification - ctx.CertificateBundleEnvironment.Add("REQUESTS_CA_BUNDLE"); - } + if (ctx.Scope != CertificateTrustScope.Append) + { + // Override default certificates path for the requests module. + // See: https://docs.python-requests.org/en/latest/user/advanced/#ssl-cert-verification + ctx.CertificateBundleEnvironment.Add("REQUESTS_CA_BUNDLE"); + + // Override default certificates path for Python modules that honor OpenSSL style paths. + // This has been tested with urllib, urllib3, httpx, and aiohttp. + // See: https://docs.openssl.org/3.0/man3/SSL_CTX_load_verify_locations/#description + ctx.CertificateBundleEnvironment.Add("SSL_CERT_FILE"); + } - // Override default opentelemetry-python certificate bundle path - // See: https://opentelemetry-python.readthedocs.io/en/latest/exporter/otlp/otlp.html#module-opentelemetry.exporter.otlp - ctx.CertificateBundleEnvironment.Add("OTEL_EXPORTER_OTLP_CERTIFICATE"); + // Override default opentelemetry-python certificate bundle path + // See: https://opentelemetry-python.readthedocs.io/en/latest/exporter/otlp/otlp.html#module-opentelemetry.exporter.otlp + ctx.CertificateBundleEnvironment.Add("OTEL_EXPORTER_OTLP_CERTIFICATE"); - return Task.CompletedTask; - }); + return Task.CompletedTask; + }); // VS Code debug support - only applicable for Script and Module types if (entrypointType is EntrypointType.Script or EntrypointType.Module) diff --git a/src/Aspire.Hosting/ApplicationModel/CertificateAuthorityCollectionAnnotation.cs b/src/Aspire.Hosting/ApplicationModel/CertificateAuthorityCollectionAnnotation.cs index 8b67ce5203e..0d4deb0006e 100644 --- a/src/Aspire.Hosting/ApplicationModel/CertificateAuthorityCollectionAnnotation.cs +++ b/src/Aspire.Hosting/ApplicationModel/CertificateAuthorityCollectionAnnotation.cs @@ -4,18 +4,32 @@ namespace Aspire.Hosting.ApplicationModel; /// -/// Defines the scope of custom certificate authorities for a resource. The default is . +/// Defines the scope of custom certificate authorities for a resource. The default scope for most resources +/// is , but some resources may choose to override this default behavior. /// -public enum CustomCertificateAuthoritiesScope +public enum CertificateTrustScope { /// - /// Append the specified certificate authorities to the default set of trusted CAs for a resource. + /// Append the specified certificate authorities to the default set of trusted CAs for a resource. Not all + /// resources support this mode, in which case custom certificate authorities may not be applied. In that case, + /// consider using or instead. /// Append, /// /// Replace the default set of trusted CAs for a resource with the specified certificate authorities. /// Override, + /// + /// Attempt to configure the resource to trust the default system certificate authorities in addition to + /// any configured custom certificate trust. This mode is useful for resources that don't otherwise + /// allow appending to their default trusted certificate authorities but do allow overriding the set + /// of trusted certificates (e.g. Python, Rust, etc.). + /// + System, + /// + /// Disable all custom certificate authority configuration for a resource. + /// + None, } /// @@ -37,5 +51,5 @@ public sealed class CertificateAuthorityCollectionAnnotation : IResourceAnnotati /// Gets a value indicating whether the resource should attempt to override its default CA trust behavior in /// favor of the provided certificates (not all resources will support this). /// - public CustomCertificateAuthoritiesScope? Scope { get; internal set; } + public CertificateTrustScope? Scope { get; internal set; } } diff --git a/src/Aspire.Hosting/ApplicationModel/ContainerCertificateTrustCallbackAnnotation.cs b/src/Aspire.Hosting/ApplicationModel/ContainerCertificateTrustCallbackAnnotation.cs index 602f514143a..4d051e9b3de 100644 --- a/src/Aspire.Hosting/ApplicationModel/ContainerCertificateTrustCallbackAnnotation.cs +++ b/src/Aspire.Hosting/ApplicationModel/ContainerCertificateTrustCallbackAnnotation.cs @@ -28,9 +28,9 @@ public sealed class ContainerCertificateTrustCallbackAnnotationContext public required IResource Resource { get; init; } /// - /// Gets the of trust for the resource. + /// Gets the of trust for the resource. /// - public required CustomCertificateAuthoritiesScope Scope { get; init; } + public required CertificateTrustScope Scope { get; init; } /// /// Gets the of certificates for this resource. @@ -72,7 +72,7 @@ public sealed class ContainerCertificateTrustCallbackAnnotationContext /// /// List of default certificate bundle files in the container that will be replaced if the resource scope of trust is - /// set to . Defaults to common Linux paths for CA certificates + /// set to . Defaults to common Linux paths for CA certificates /// to maximize compatibility, but can be overriden with specific paths for a given resource if needed. /// See: https://go.dev/src/crypto/x509/root_linux.go /// @@ -94,7 +94,7 @@ public sealed class ContainerCertificateTrustCallbackAnnotationContext /// /// List of default certificate directories in a container that should be appended to the custom certificate directories in - /// mode. Defaults to common Linux paths for CA certificates. + /// mode. Defaults to common Linux paths for CA certificates. /// See: https://go.dev/src/crypto/x509/root_linux.go /// public List DefaultContainerCertificatesDirectoryPaths { get; } = new() diff --git a/src/Aspire.Hosting/ApplicationModel/ExecutableCertificateTrustCallbackAnnotation.cs b/src/Aspire.Hosting/ApplicationModel/ExecutableCertificateTrustCallbackAnnotation.cs index 0fee2619763..fd4d87a2475 100644 --- a/src/Aspire.Hosting/ApplicationModel/ExecutableCertificateTrustCallbackAnnotation.cs +++ b/src/Aspire.Hosting/ApplicationModel/ExecutableCertificateTrustCallbackAnnotation.cs @@ -28,9 +28,9 @@ public sealed class ExecutableCertificateTrustCallbackAnnotationContext public required IResource Resource { get; init; } /// - /// Gets the of trust for the resource. + /// Gets the of trust for the resource. /// - public required CustomCertificateAuthoritiesScope Scope { get; init; } + public required CertificateTrustScope Scope { get; init; } /// /// Gets the of certificates for this resource. diff --git a/src/Aspire.Hosting/Dcp/DcpExecutor.cs b/src/Aspire.Hosting/Dcp/DcpExecutor.cs index 54326438ecc..acb56f39474 100644 --- a/src/Aspire.Hosting/Dcp/DcpExecutor.cs +++ b/src/Aspire.Hosting/Dcp/DcpExecutor.cs @@ -2055,7 +2055,7 @@ await modelResource.ProcessContainerRuntimeArgValues( bool trustDevCert = _distributedApplicationOptions.TrustDeveloperCertificate; var certificates = new X509Certificate2Collection(); - var scope = CustomCertificateAuthoritiesScope.Append; + var scope = CertificateTrustScope.Append; if (modelResource.TryGetLastAnnotation(out var caAnnotation)) { foreach (var certCollection in caAnnotation.CertificateAuthorityCollections) @@ -2067,6 +2067,17 @@ await modelResource.ProcessContainerRuntimeArgValues( scope = caAnnotation.Scope.GetValueOrDefault(scope); } + if (scope == CertificateTrustScope.None) + { + return (new List(), new List(), false); + } + + if (scope == CertificateTrustScope.System) + { + // Read the system root certificates and add them to the collection + certificates.AddRootCertificates(); + } + if (trustDevCert) { foreach (var cert in _developerCertificateService.Certificates) @@ -2164,7 +2175,7 @@ await modelResource.ProcessContainerRuntimeArgValues( bool trustDevCert = _distributedApplicationOptions.TrustDeveloperCertificate; var certificates = new X509Certificate2Collection(); - var scope = CustomCertificateAuthoritiesScope.Append; + var scope = CertificateTrustScope.Append; if (modelResource.TryGetLastAnnotation(out var caAnnotation)) { foreach (var certCollection in caAnnotation.CertificateAuthorityCollections) @@ -2176,6 +2187,18 @@ await modelResource.ProcessContainerRuntimeArgValues( scope = caAnnotation.Scope.GetValueOrDefault(scope); } + if (scope == CertificateTrustScope.None) + { + // Resource has disabled custom certificate authorities + return (new List(), new List(), new List(), false); + } + + if (scope == CertificateTrustScope.System) + { + // Read the system root certificates and add them to the collection + certificates.AddRootCertificates(); + } + if (trustDevCert) { foreach (var cert in _developerCertificateService.Certificates) @@ -2191,10 +2214,11 @@ await modelResource.ProcessContainerRuntimeArgValues( Certificates = certificates, CancellationToken = cancellationToken }; - if (scope == CustomCertificateAuthoritiesScope.Override) + + if (scope != CertificateTrustScope.Append) { - // Override default OpenSSL certificate bundle path resolution - // SSL_CERT_FILE is always added to the defaults when the scope is Override + // When Override or System scope is set (not Append), override the default OpenSSL certificate bundle path + // resolution by setting the SSL_CERT_FILE environment variable. // See: https://docs.openssl.org/3.0/man3/SSL_CTX_load_verify_locations/#description context.CertificateBundleEnvironment.Add("SSL_CERT_FILE"); } @@ -2254,7 +2278,7 @@ await modelResource.ProcessContainerRuntimeArgValues( } var caDirEnvValue = caFilesPath; - if (scope == CustomCertificateAuthoritiesScope.Append) + if (scope == CertificateTrustScope.Append) { foreach (var defaultCaDir in context.DefaultContainerCertificatesDirectoryPaths) { @@ -2313,9 +2337,9 @@ await modelResource.ProcessContainerRuntimeArgValues( ], }); - if (scope == CustomCertificateAuthoritiesScope.Override) + if (scope != CertificateTrustScope.Append) { - // If overriding the system CA bundle, then we want to copy our bundle to the well-known locations + // If overriding the default resource CA bundle, then we want to copy our bundle to the well-known locations // used by common Linux distributions to make it easier to ensure applications pick it up. foreach (var bundlePath in context.DefaultContainerCertificateAuthorityBundlePaths) { diff --git a/src/Aspire.Hosting/ResourceBuilderExtensions.cs b/src/Aspire.Hosting/ResourceBuilderExtensions.cs index 62be4407397..188c016a5c7 100644 --- a/src/Aspire.Hosting/ResourceBuilderExtensions.cs +++ b/src/Aspire.Hosting/ResourceBuilderExtensions.cs @@ -2156,16 +2156,16 @@ public static IResourceBuilder WithDeveloperCertificateTrust - /// Sets the for custom certificate authorities associated with the resource. + /// Sets the for custom certificate authorities associated with the resource. /// /// The type of the resource. /// The resource builder. /// The scope to apply to custom certificate authorities associated with the resource. /// The . /// - /// The default scope is which means that custom certificate authorities - /// should be appended to the default trusted certificate authorities for the resource. Setting the scope to - /// indicates the set of certificates in referenced + /// The default scope if not overridden is which means that custom certificate + /// authorities should be appended to the default trusted certificate authorities for the resource. Setting the scope to + /// indicates the set of certificates in referenced /// (and optionally Aspire developer certificiates) should be used as the /// exclusive source of trust for a resource. /// In all cases, this is a best effort implementation as not all resources support full customization of certificate @@ -2178,11 +2178,11 @@ public static IResourceBuilder WithDeveloperCertificateTrust /// /// - public static IResourceBuilder WithCustomCertificateAuthoritiesScope(this IResourceBuilder builder, CustomCertificateAuthoritiesScope scope) + public static IResourceBuilder WithCertificateTrustScope(this IResourceBuilder builder, CertificateTrustScope scope) where TResource : IResourceWithEnvironment, IResourceWithArgs { ArgumentNullException.ThrowIfNull(builder); diff --git a/src/Aspire.Hosting/Utils/X509Certificate2Extensions.cs b/src/Aspire.Hosting/Utils/X509Certificate2Extensions.cs index 7781b0e3c11..1531556bf99 100644 --- a/src/Aspire.Hosting/Utils/X509Certificate2Extensions.cs +++ b/src/Aspire.Hosting/Utils/X509Certificate2Extensions.cs @@ -78,4 +78,26 @@ public static bool SupportsContainerTrust(this X509Certificate2 certificate) return true; } + + /// + /// Adds certificates from the system root stores to the specified collection. + /// + /// The to add the certificates to. + public static void AddRootCertificates(this X509Certificate2Collection collection) + { + ArgumentNullException.ThrowIfNull(collection); + + var locations = new[] + { + (StoreName.Root, StoreLocation.CurrentUser), + (StoreName.Root, StoreLocation.LocalMachine), + }; + + foreach (var (storeName, storeLocation) in locations) + { + using var store = new X509Store(storeName, storeLocation); + store.Open(OpenFlags.ReadOnly); + collection.AddRange(store.Certificates); + } + } } \ No newline at end of file