File: ApplicationModel\CertificateTrustConfigurationCallbackAnnotation.cs
Web Access
Project: src\src\Aspire.Hosting\Aspire.Hosting.csproj (Aspire.Hosting)
// 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 System.Security.Cryptography.X509Certificates;
 
namespace Aspire.Hosting.ApplicationModel;
 
/// <summary>
/// An annotation that indicates a resource wants to manage how custom certificate trust is configured.
/// </summary>
/// <param name="callback">The callback used to customize certificate trust for the resource.</param>
public sealed class CertificateTrustConfigurationCallbackAnnotation(Func<CertificateTrustConfigurationCallbackAnnotationContext, Task> callback) : IResourceAnnotation
{
    /// <summary>
    /// Gets the callback to invoke to populate or modify the certificate authority collection.
    /// </summary>
    public Func<CertificateTrustConfigurationCallbackAnnotationContext, Task> Callback { get; } = callback ?? throw new ArgumentNullException(nameof(callback));
}
 
/// <summary>
/// Context provided to a <see cref="CertificateTrustConfigurationCallbackAnnotation"/> callback.
/// </summary>
public sealed class CertificateTrustConfigurationCallbackAnnotationContext
{
    /// <summary>
    /// Gets the <see cref="DistributedApplicationExecutionContext"/> for this session.
    /// </summary>
    public required DistributedApplicationExecutionContext ExecutionContext { get; init; }
 
    /// <summary>
    /// Gets the resource to which the annotation is applied.
    /// </summary>
    public required IResource Resource { get; init; }
 
    /// <summary>
    /// Gets the command line arguments associated with the callback context. Values can be either a string or a path
    /// value provider such as <see cref="CertificateBundlePath"/> or <see cref="CertificateDirectoriesPath"/>.
    /// </summary>
    /// <remarks>
    /// <example>
    /// <code language="csharp">
    /// builder.AddContainer("my-resource", "my-image:latest")
    ///    .WithCertificateTrustConfigurationCallback(ctx =>
    ///    {
    ///        ctx.Arguments.Add("--use-system-ca");
    ///        return Task.CompletedTask;
    ///    });
    /// </code>
    /// </example>
    /// </remarks>
    public required List<object> Arguments { get; init; }
 
    /// <summary>
    /// Gets the environment variables required to configure certificate trust for the resource.
    /// The dictionary key is the environment variable name; the value can be either a string or a path
    /// value provider such as <see cref="CertificateBundlePath"/> or <see cref="CertificateDirectoriesPath"/>.
    /// By default the environment will always include an entry for `SSL_CERT_DIR` and may include `SSL_CERT_FILE` if
    /// <see cref="CertificateTrustScope.Override"/> or <see cref="CertificateTrustScope.System"/> is configured.
    /// </summary>
    /// <remarks>
    /// <example>
    /// <code language="csharp">
    /// builder.AddContainer("my-resource", "my-image:latest")
    ///     .WithCertificateTrustConfigurationCallback(ctx =>
    ///     {
    ///         ctx.EnvironmentVariables["MY_CUSTOM_CERT_VAR"] = ctx.CertificateBundlePath;
    ///         ctx.EnvironmentVariables["CERTS_DIR"] = ctx.CertificateDirectoriesPath;
    ///         return Task.CompletedTask;
    ///     });
    /// </code>
    /// </example>
    /// </remarks>
    public required Dictionary<string, object> EnvironmentVariables { get; init; }
 
    /// <summary>
    /// A value provider that will resolve to a path to a custom certificate bundle.
    /// </summary>
    public required ReferenceExpression CertificateBundlePath { get; init; }
 
    /// <summary>
    /// A value provider that will resolve to paths containing individual certificates.
    /// </summary>
    public required ReferenceExpression CertificateDirectoriesPath { get; init; }
 
    /// <summary>
    /// Gets the <see cref="CertificateTrustScope"/> for the resource.
    /// </summary>
    public required CertificateTrustScope Scope { get; init; }
 
    /// <summary>
    /// Gets the <see cref="CancellationToken"/> that can be used to cancel the operation.
    /// </summary>
    public required CancellationToken CancellationToken { get; init; }
 
    /// <summary>
    /// Adds a custom certificate bundle to the callback context. The provided generator will be invoked during trust configuration and should return the bundle contents as a byte array.
    /// </summary>
    /// <param name="bundleGenerator">A function that generates the custom certificate bundle and returns the bundle contents as a byte array.</param>
    /// <returns>A <see cref="ReferenceExpression"/> that can be used to reference the custom bundle.</returns>
    /// <remarks>
    /// <example>
    /// <code language="csharp">
    /// builder.AddContainer("my-java-app", "my-image:latest")
    ///     .WithCertificateTrustConfiguration(ctx =>
    ///     {
    ///         ctx.EnvironmentVariables["JAVAX_NET_SSL_TRUSTSTORE"] = ctx.CreateCustomBundle((certs, ct) =>
    ///         {
    ///             var pkcs12Builder = new Pkcs12Builder();
    ///             var safeContents = new Pkcs12SafeContents();
    ///             foreach (var cert in certs)
    ///             {
    ///                 safeContents.AddCertificate(cert);
    ///             }
    ///             pkcs12Builder.AddSafeContentsUnencrypted(safeContents);
    ///             pkcs12Builder.SealWithMac(string.Empty, HashAlgorithmName.SHA256, 2048);
    ///             return Task.FromResult(pkcs12Builder.Encode());
    ///         });
    ///         return Task.CompletedTask;
    ///     });
    /// </code>
    /// </example>
    /// </remarks>
    [Experimental("ASPIRECERTIFICATES001", UrlFormat = "https://aka.ms/aspire/diagnostics/{0}")]
    public ReferenceExpression CreateCustomBundle(Func<X509Certificate2Collection, CancellationToken, Task<byte[]>> bundleGenerator)
    {
        ArgumentException.ThrowIfNullOrEmpty(RootCertificatesPath);
 
        var bundleId = Guid.NewGuid().ToString("N");
        CustomBundlesFactories[bundleId] = bundleGenerator;
 
        var bundleFilename = IsContainer ? $"{RootCertificatesPath}/bundles/{bundleId}" : Path.Join(RootCertificatesPath, "bundles", bundleId);
        var reference = ReferenceExpression.Create($"{bundleFilename}");
 
        return reference;
    }
 
    /// <summary>
    /// Gets the root path where certificates will be written for the resource.
    /// </summary>
    internal string? RootCertificatesPath { get; init; }
 
    /// <summary>
    /// Is this being generated for a container (requires Linux style paths)
    /// </summary>
    internal bool IsContainer { get; init; }
 
    /// <summary>
    /// Collection of custom certificate bundle generators added via the <see cref="CreateCustomBundle"/> method, keyed by the bundle's unique ID/filename under the root certificates path. The value is a function that generates the bundle contents as a byte array given a collection of X509 certificates.
    /// </summary>
    internal Dictionary<string, Func<X509Certificate2Collection, CancellationToken, Task<byte[]>>> CustomBundlesFactories { get; } = new();
}