Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -113,12 +113,12 @@ public static IResourceBuilder<KeycloakResource> AddKeycloak(
bool addHttps = false;
if (!resource.TryGetLastAnnotation<ServerAuthenticationCertificateAnnotation>(out var annotation))
{
if (developerCertificateService.DefaultTlsTerminationEnabled)
if (developerCertificateService.UseForServerAuthentication)
{
addHttps = true;
}
}
else if (annotation.UseDeveloperCertificate.GetValueOrDefault(developerCertificateService.DefaultTlsTerminationEnabled) || annotation.Certificate is not null)
else if (annotation.UseDeveloperCertificate.GetValueOrDefault(developerCertificateService.UseForServerAuthentication) || annotation.Certificate is not null)
{
addHttps = true;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -320,14 +320,14 @@ public static IResourceBuilder<UvicornAppResource> AddUvicornApp(
bool addHttps = false;
if (!resourceBuilder.Resource.TryGetLastAnnotation<ServerAuthenticationCertificateAnnotation>(out var annotation))
{
if (developerCertificateService.DefaultTlsTerminationEnabled)
if (developerCertificateService.UseForServerAuthentication)
{
// If no certificate is configured, and the developer certificate service supports container trust,
// configure the resource to use the developer certificate for its key pair.
addHttps = true;
}
}
else if (annotation.UseDeveloperCertificate.GetValueOrDefault(developerCertificateService.DefaultTlsTerminationEnabled) || annotation.Certificate is not null)
else if (annotation.UseDeveloperCertificate.GetValueOrDefault(developerCertificateService.UseForServerAuthentication) || annotation.Certificate is not null)
{
addHttps = true;
}
Expand Down
8 changes: 4 additions & 4 deletions src/Aspire.Hosting.Redis/RedisBuilderExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -180,12 +180,12 @@ public static IResourceBuilder<RedisResource> AddRedis(
bool addHttps = false;
if (!redis.TryGetLastAnnotation<ServerAuthenticationCertificateAnnotation>(out var annotation))
{
if (developerCertificateService.DefaultTlsTerminationEnabled)
if (developerCertificateService.UseForServerAuthentication)
{
addHttps = true;
}
}
else if (annotation.UseDeveloperCertificate.GetValueOrDefault(developerCertificateService.DefaultTlsTerminationEnabled) || annotation.Certificate is not null)
else if (annotation.UseDeveloperCertificate.GetValueOrDefault(developerCertificateService.UseForServerAuthentication) || annotation.Certificate is not null)
{
addHttps = true;
}
Expand Down Expand Up @@ -390,12 +390,12 @@ public static IResourceBuilder<RedisResource> WithRedisInsight(this IResourceBui
bool addHttps = false;
if (!resource.TryGetLastAnnotation<ServerAuthenticationCertificateAnnotation>(out var annotation))
{
if (developerCertificateService.DefaultTlsTerminationEnabled)
if (developerCertificateService.UseForServerAuthentication)
{
addHttps = true;
}
}
else if (annotation.UseDeveloperCertificate.GetValueOrDefault(developerCertificateService.DefaultTlsTerminationEnabled) || annotation.Certificate is not null)
else if (annotation.UseDeveloperCertificate.GetValueOrDefault(developerCertificateService.UseForServerAuthentication) || annotation.Certificate is not null)
{
addHttps = true;
}
Expand Down
4 changes: 2 additions & 2 deletions src/Aspire.Hosting.Yarp/YarpResourceExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -62,13 +62,13 @@ public static IResourceBuilder<YarpResource> AddYarp(
bool addHttps = false;
if (!resource.TryGetLastAnnotation<ServerAuthenticationCertificateAnnotation>(out var annotation))
{
if (developerCertificateService.DefaultTlsTerminationEnabled)
if (developerCertificateService.UseForServerAuthentication)
{
// If no specific certificate is configured
addHttps = true;
}
}
else if (annotation.UseDeveloperCertificate.GetValueOrDefault(developerCertificateService.DefaultTlsTerminationEnabled) || annotation.Certificate is not null)
else if (annotation.UseDeveloperCertificate.GetValueOrDefault(developerCertificateService.UseForServerAuthentication) || annotation.Certificate is not null)
{
addHttps = true;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
// The .NET Foundation licenses this file to you under the MIT license.

using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;

namespace Aspire.Hosting.ApplicationModel;

Expand Down Expand Up @@ -276,4 +277,37 @@ public sealed class ContainerFileSystemCallbackContext
/// The app model resource the callback is associated with.
/// </summary>
public required IResource Model { get; init; }

/// <summary>
/// The path to the server authentication certificate file inside the container.
/// </summary>
Comment on lines +282 to +283

Copilot AI Nov 24, 2025

Copy link

Choose a reason for hiding this comment

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

The documentation says "The path to the server authentication certificate file inside the container" but this property actually contains a ContainerFileSystemCallbackServerAuthenticationCertificateContext object, not a path. The documentation should describe what the context contains, e.g., "The server authentication certificate context containing paths to certificate files and associated configuration."

Suggested change
/// The path to the server authentication certificate file inside the container.
/// </summary>
/// Gets or sets the server authentication certificate context containing paths to certificate files and associated configuration for use inside the container.
/// </summary>
/// <remarks>
/// The <see cref="ContainerFileSystemCallbackServerAuthenticationCertificateContext"/> provides access to the certificate path, key path, PFX path, and password for server authentication within the container.
/// </remarks>

Copilot uses AI. Check for mistakes.
[Experimental("ASPIRECERTIFICATES001", UrlFormat = "https://aka.ms/aspire/diagnostics/{0}")]
public ContainerFileSystemCallbackServerAuthenticationCertificateContext? ServerAuthenticationCertificateContext { get; set; }
}

/// <summary>
/// Represents the context for server authentication certificate files in a <see cref="ContainerFileSystemCallbackContext"/>.
/// </summary>
[Experimental("ASPIRECERTIFICATES001", UrlFormat = "https://aka.ms/aspire/diagnostics/{0}")]
public sealed class ContainerFileSystemCallbackServerAuthenticationCertificateContext
{
/// <summary>
/// The path to the server authentication certificate file inside the container.
/// </summary>
Comment thread
danegsta marked this conversation as resolved.
public ReferenceExpression CertificatePath { get; init; } = null!;

/// <summary>
/// The path to the server authentication key file inside the container.
/// </summary>
Comment thread
danegsta marked this conversation as resolved.
public ReferenceExpression KeyPath { get; init; } = null!;

/// <summary>
/// The path to the server authentication PFX file inside the container.
/// </summary>
Comment on lines +306 to +309

Copilot AI Nov 24, 2025

Copy link

Choose a reason for hiding this comment

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

According to the XML documentation guidelines, the documentation for public API properties should include more context. The PfxPath property should clarify that it's a ReferenceExpression that resolves to the path. Consider: "Gets the reference expression that resolves to the path of the server authentication PFX (PKCS#12) file inside the container."

Copilot generated this review using guidance from repository custom instructions.
public ReferenceExpression PfxPath { get; init; } = null!;

/// <summary>
/// The password for the server authentication PFX file inside the container.
/// </summary>
Comment thread
danegsta marked this conversation as resolved.
public string? Password { get; init; }
}
2 changes: 2 additions & 0 deletions src/Aspire.Hosting/ApplicationModel/ExpressionResolver.cs
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@ async Task<ResolvedValue> EvalExpressionAsync(ReferenceExpression expr, ValuePro
var args = new object?[expr.ValueProviders.Count];
var isSensitive = false;

expr.WasResolved = true;

for (var i = 0; i < expr.ValueProviders.Count; i++)
{
var result = await ResolveInternalAsync(expr.ValueProviders[i], context).ConfigureAwait(false);
Expand Down
7 changes: 7 additions & 0 deletions src/Aspire.Hosting/ApplicationModel/ReferenceExpression.cs
Original file line number Diff line number Diff line change
Expand Up @@ -63,13 +63,20 @@ private ReferenceExpression(string format, IValueProvider[] valueProviders, stri
public string ValueExpression =>
string.Format(CultureInfo.InvariantCulture, Format, _manifestExpressions);

/// <summary>
/// Indicates whether this expression was ever referenced to get its value.
/// </summary>
internal bool WasResolved { get; set; }

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Why do we need this?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

I’m only generating key material that’s actually reference; if no resource actually references the pfx or pem key, I’m not exporting them. I’m using this to check the usage.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I dont like it, can we put it outside of the reference expression.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Other option (without making the API weird) would be a custom IValueProvider


/// <summary>
/// Gets the value of the expression. The final string value after evaluating the format string and its parameters.
/// </summary>
/// <param name="context">A context for resolving the value.</param>
/// <param name="cancellationToken">A <see cref="CancellationToken"/>.</param>
public async ValueTask<string?> GetValueAsync(ValueProviderContext context, CancellationToken cancellationToken)
{
WasResolved = true;

// NOTE: any logical changes to this method should also be made to ExpressionResolver.EvalExpressionAsync
if (Format.Length == 0)
{
Expand Down
62 changes: 45 additions & 17 deletions src/Aspire.Hosting/ApplicationModel/ResourceExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -485,12 +485,7 @@ internal class ResourceConfigurationContext
/// <summary>
/// The server authentication certificate for the resource, if any.
/// </summary>
public X509Certificate2? ServerAuthCertificate { get; init; }

/// <summary>
/// The password for the server authentication certificate, if any.
/// </summary>
public string? ServerAuthPassword { get; init; }
public ServerAuthenticationCertificateConfigurationDetails? ServerAuthenticationCertificateConfiguration { get; init; }

/// <summary>
/// Any exception that occurred during the configuration processing.
Expand Down Expand Up @@ -547,11 +542,10 @@ internal static async ValueTask<ResourceConfigurationContext> ProcessConfigurati
cancellationToken).ConfigureAwait(false);
}

X509Certificate2? serverAuthCertificate = null;
string? serverAuthPassword = null;
ServerAuthenticationCertificateConfigurationDetails? serverAuthCertificateConfiguration = null;
if (withServerAuthCertificateConfig)
{
(args, envVars, serverAuthCertificate, serverAuthPassword) = await resource.GatherServerAuthCertificateConfigAsync(
(args, envVars, serverAuthCertificateConfiguration) = await resource.GatherServerAuthCertificateConfigAsync(
executionContext,
args,
envVars,
Expand Down Expand Up @@ -616,8 +610,7 @@ await ProcessGatheredEnvironmentVariableValuesAsync(
EnvironmentVariables = resolvedEnvVars,
CertificateTrustScope = certificateTrustScope,
TrustedCertificates = trustedCertificates!,
ServerAuthCertificate = serverAuthCertificate,
ServerAuthPassword = serverAuthPassword,
ServerAuthenticationCertificateConfiguration = serverAuthCertificateConfiguration,
Exception = exception,
};
}
Expand Down Expand Up @@ -751,6 +744,32 @@ internal class ServerAuthCertificateConfigBuilderContext
public required ReferenceExpression PfxPath { get; init; }
}

/// <summary>
/// Holds the details of server authentication certificate configuration.
/// </summary>
internal sealed class ServerAuthenticationCertificateConfigurationDetails
{
/// <summary>
/// The server authentication certificate for the resource, if any.
/// </summary>
public required X509Certificate2 Certificate { get; init; }

/// <summary>
/// Indicates whether the resource references a PEM key for server authentication.
/// </summary>
public required ReferenceExpression KeyPathReference { get; set; }

/// <summary>
/// Indicates whether the resource references a PFX file for server authentication.
/// </summary>
public required ReferenceExpression PfxReference { get; set; }

/// <summary>
/// The passphrase for the server authentication certificate, if any.
/// </summary>
public string? Password { get; init; }
}

/// <summary>
/// Gathers server authentication certificate configuration for the specified resource within the given execution context.
/// </summary>
Expand All @@ -760,8 +779,8 @@ internal class ServerAuthCertificateConfigBuilderContext
/// <param name="environmentVariables">Existing environment variables that will be used to initialize the context for the config callback.</param>
/// <param name="certificateConfigContextFactory">A factory function to create the context for building server authentication certificate configuration; provides the paths for the certificate, key, and PFX files.</param>
/// <param name="cancellationToken">A token to monitor for cancellation requests.</param>
/// <returns>The resulting command line arguments, environment variables, and optionally the certificate and passphrase for server auth.</returns>
internal static async ValueTask<(List<object> arguments, Dictionary<string, object> environmentVariables, X509Certificate2? serverAuthCertificate, string? passphrase)> GatherServerAuthCertificateConfigAsync(
/// <returns>The resulting command line arguments, environment variables, and optionally the specific server authentication certificate configuration details.</returns>
internal static async ValueTask<(List<object> arguments, Dictionary<string, object> environmentVariables, ServerAuthenticationCertificateConfigurationDetails? details)> GatherServerAuthCertificateConfigAsync(
this IResource resource,
DistributedApplicationExecutionContext executionContext,
List<object> arguments,
Expand All @@ -778,14 +797,14 @@ internal class ServerAuthCertificateConfigBuilderContext
if (effectiveAnnotation is null)
{
// Should never happen
return (arguments, environmentVariables, null, null);
return (arguments, environmentVariables, null);
}

X509Certificate2? certificate = effectiveAnnotation.Certificate;
if (certificate is null)
{
var developerCertificateService = executionContext.ServiceProvider.GetRequiredService<IDeveloperCertificateService>();
if (effectiveAnnotation.UseDeveloperCertificate.GetValueOrDefault(developerCertificateService.DefaultTlsTerminationEnabled))
if (effectiveAnnotation.UseDeveloperCertificate.GetValueOrDefault(developerCertificateService.UseForServerAuthentication))
{
certificate = developerCertificateService.Certificates.FirstOrDefault();
}
Expand All @@ -794,7 +813,7 @@ internal class ServerAuthCertificateConfigBuilderContext
if (certificate is null)
{
// No certificate to configure, do nothing
return (arguments, environmentVariables, null, null);
return (arguments, environmentVariables, null);
}

var configBuilderContext = certificateConfigContextFactory(certificate);
Expand All @@ -819,7 +838,16 @@ internal class ServerAuthCertificateConfigBuilderContext

string? password = effectiveAnnotation.Password is not null ? await effectiveAnnotation.Password.GetValueAsync(cancellationToken).ConfigureAwait(false) : null;

return (arguments, environmentVariables, certificate, password);
return (
arguments,
environmentVariables,
new ServerAuthenticationCertificateConfigurationDetails()
{
Certificate = certificate,
Password = password,
KeyPathReference = context.KeyPath,
PfxReference = context.PfxPath,
});
}

internal static async ValueTask<ResolvedValue?> ResolveValueAsync(
Expand Down
21 changes: 21 additions & 0 deletions src/Aspire.Hosting/ApplicationModel/X509CertificateResource.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Diagnostics.CodeAnalysis;
using Aspire.Hosting.ApplicationModel;

/// <summary>
/// Represents a X509 Certificate resource. This may be backed by a local certificate in run mode or a remote certificate in deploy mode.
/// </summary>
Comment thread
danegsta marked this conversation as resolved.
Outdated
[Experimental("ASPIRECERTIFICATES001", UrlFormat = "https://aka.ms/aspire/diagnostics/{0}")]
public sealed class X509CertificateResource : Resource
{
/// <summary>
/// Initializes a new instance of the <see cref="X509CertificateResource"/> class with the specified name.
/// </summary>
/// <param name="name">The name of the resource.</param>
public X509CertificateResource(string name) : base(name)
{
ArgumentNullException.ThrowIfNull(name);
Comment thread
danegsta marked this conversation as resolved.
Outdated
}
}
Loading
Loading